0 / 91
Week 11 · Day 72 of 91

Frontend and backend unit tests

Testing, Security, Docker, and Deployment

Objective

Write deterministic tests for pure logic and component behavior.

Production readiness resembles validation before field deployment: repeatable setup, protective limits, test evidence, monitoring, and rollback plans.

  • arrange, act, assert
  • mocks only when useful
  • behavior-focused assertions

Why this matters

Yesterday you decided what to prove. Today you prove the unit-level rows: pure logic on the server and one component's behaviour in the browser. By the end of the hour your test suite will have a property that matters more than the number of tests — when you deliberately break a rule, a test fails and names it. A suite that stays green while the app is broken is worse than no suite, because it hands you false confidence.

Deterministic tests

Deterministic means: same input, same result, every run, on every machine, in any order. A test that passes on Tuesday and fails on Wednesday teaches everyone to ignore failures, which destroys the value of the whole suite.

Three things make tests non-deterministic, and all three appear in your app:

  • The current time. new Date() gives a different answer every run, so a test using it is quietly testing the calendar. Fix: make the function take the date as a parameter.
  • Random values. IDs, tokens, shuffles. Fix: pass them in, or assert the shape rather than the value.
  • Shared state. One test leaves data behind, the next one sees it. Fix: reset between tests.

Fix the bench conditions before you trust the reading

Measuring a temperature-sensitive circuit in an uncontrolled room gives you a different number every hour, and none of them are the component's fault. You clamp the supply, fix the ambient, and then the reading means something. Passing the date in as a parameter is clamping the supply: you remove the drifting input so the measurement is about the circuit.

Arrange, act, assert

Every good test has the same three-part shape, in this order. Keeping them visibly separate makes a test readable in five seconds.

  • Arrange — set up the inputs and the world the code needs.
  • Act — call the thing once. Exactly one action; if you need two, you probably want two tests.
  • Assert — state what must now be true.
import { describe, it, expect } from "vitest";
import { statusLabel } from "../src/rules.js";

describe("statusLabel", () => {
  it("reports 'Due soon' within seven days of the due date", () => {
    const lastService = "2026-01-01"; // arrange
    const intervalDays = 30;
    const today = "2026-01-26";

    const label = statusLabel(lastService, intervalDays, today); // act

    expect(label).toBe("Due soon"); // assert
  });
});

The test name is part of the test. it("works") tells a future reader nothing when it fails at 2am. Name the behaviour, in words a non-programmer could check: reports 'Due soon' within seven days of the due date.

Behaviour-focused assertions

Assert what a caller can observe, not how the answer was produced. Compare:

expect(statusLabel("2026-01-01", 30, "2026-02-05")).toBe("Overdue"); // behaviour
expect(rules.daysBetweenCalls).toBe(2);                              // implementation

The first still passes after you rewrite the internals; the second breaks and tells you nothing. Useful matchers, all from Vitest: toBe for exact primitives, toEqual for deep object or array comparison, toContain for membership, toThrow for expected errors, and toBeCloseTo for floating-point numbers (0.1 + 0.2 is not exactly 0.3, so toBe fails there).

Mocks, only when useful

A mock is a stand-in you substitute for a real dependency so the test can control it. It is a real tool with a narrow purpose: use one when the real thing is slow, unavailable, unpredictable, or has side effects you must not cause — sending email, charging a card, calling a paid API.

Do not mock your own logic to make a test pass. Every mock is an assumption that the real thing behaves the way you imagined, and that assumption is untested. A suite of heavily mocked tests can be 100% green while the app is entirely broken, because you only ever tested your own guesses. For the database in particular, do not mock — test it for real tomorrow.

The stunt double

You use a stunt double for the one shot that would genuinely injure the actor. You do not film the whole movie with doubles and then claim the actor gave a great performance.

Testing a component's behaviour

React components are tested the same way, with one shift: the observable behaviour is what the user sees and can do, not the component's state variables. You render it, interact the way a user would, and assert on visible text and roles. Testing Library is built around exactly that idea, and its query names — getByRole, getByLabelText — push you toward user-visible things.

Walkthrough: install, run, then break it on purpose

Install the test tools in your project. -D means development dependency — needed to build and test, not to run in production.

npm install -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event

jsdom is a simulated browser environment for Node, so component tests can render without opening a real browser. Create vitest.config.js:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: ["./tests/setup.js"],
  },
});

And tests/setup.js, which clears the rendered DOM after each test so tests cannot leak into one another:

import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";

afterEach(cleanup);

Add a script to package.json so the command is one word:

"scripts": {
  "test": "vitest run"
}

Write the statusLabel test from above into tests/rules.test.js, then run it:

npm test
 ✓ tests/rules.test.js (1 test) 3ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

Now the step that proves the test is real. Open src/rules.js and deliberately break the rule — change the "due soon" window from 7 days to 70. Run npm test again:

 FAIL  tests/rules.test.js > statusLabel > reports 'Due soon' within seven days of the due date
AssertionError: expected 'OK' to be 'Due soon' // Object.is equality

- Expected
+ Received

- Due soon
+ OK

 Test Files  1 failed (1)
      Tests  1 failed (1)

Read it line by line. Line 1: which file, which describe block, which behaviour. Line 2: the claim. Then - Expected (what you demanded) against + Received (what the code produced). That four-line report is what you are buying with the whole exercise. Put the 7 back and confirm green again.

Checkpoint

You have seen your suite go red for a real reason and green again. If breaking the rule had left it green, the test was asserting the wrong thing — that is the only failure mode that matters today.

Your turn

  1. Get npm test running with one passing rule test, as in the walkthrough.

  2. Validation. Test that creating equipment with an empty name is rejected and the error names the field. Include the valid case too, so you know the rule is not just refusing everything.

  3. Permissions. Test your permission function three ways: owner allowed, manager allowed, unrelated technician denied. The denial is the important one.

  4. Status labels. Test all three outcomes — "Overdue", "Due soon", "OK" — passing today in as a parameter. If your function calls new Date() internally, change it to accept a date first; that refactor is part of today's work.

  5. One form interaction. Render your equipment form, type into it, submit, and assert on what the user sees:

    import { render, screen } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    
    it("shows an error when the name is empty", async () => {
      render(<EquipmentForm onSubmit={() => {}} />);
      await userEvent.click(screen.getByRole("button", { name: /save/i }));
      expect(await screen.findByText(/name is required/i)).toBeTruthy();
    });
    
  6. The deliberate-break drill. For each of the four areas above, break the source rule, run npm test, and record which test failed and its exact message in docs/test-plan.md. Then restore the code. Any break that leaves the suite green means that behaviour is not covered — fix the test, not the code.

Pair mode — before you write each test

Today's AI mode is pair: it proposes, you verify. Before accepting any generated change, inspect the diff, run npm test, and be able to explain every changed line.

"Help me write the behaviour statement and cases before generating test code."

Insist on the behaviour statement first. If you cannot say what must be true in one sentence, you are not ready for the code, and generated tests will assert whatever the implementation happens to do — which is the exact failure Day 71 warned about.

You are done when

Every one of the four areas has a test that fails when you deliberately break the behaviour, and npm test is green with the code restored.

Common pitfalls

  • Tests that pass no matter what. Usually a missing await, or asserting something trivially true. The deliberate-break drill is the only reliable detector.
  • Depending on new Date(). The test passes today and fails next month. Pass the date in.
  • Asserting on state instead of the screen. In a component test, if the user cannot see it, do not assert it. Assert on the rendered text or role.
  • Mocking your own module to force a pass. You then test the mock. Mock only slow, external, or irreversible things.

Verify it yourself

Open today's reference, the Vitest Getting started guide.

  1. Find the list of ways to run tests. This lesson used vitest run. What does plain vitest do differently, and why would you want that while writing code but not in a pipeline?
  2. Find expect.toEqual in the API docs and confirm the difference from toBe for two objects with identical contents. Prove it with a two-line test, and record the result.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Add tests for validation, permissions, status labels, and one form interaction.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

Tests fail when the intended behavior is deliberately broken.

Working with AI today

AI as pair programmer

Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.

Help me write the behavior statement and cases before generating test code.

References

End-of-day quiz

Q1 A valuable test should primarily verify what?
Q2 Which result best proves today’s work is complete?
Q3 Before accepting an AI-generated code change, what should you do?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.