0 / 91
Week 11 · Day 71 of 91

Testing strategy and test pyramid

Testing, Security, Docker, and Deployment

Objective

Choose the smallest test that can prove each behavior.

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

  • unit, integration, end-to-end
  • test boundaries
  • high-value paths

Why this matters

On Day 70 you demonstrated a working maintenance tracker: log in, create equipment, add a maintenance record, log out. It works today, on your machine, when you click carefully. Week 11 is about making it safe to change. The first step is not writing tests — it is deciding which behaviours are worth proving, and at which level. By the end of the hour you will have a test plan that says, for every risk in your app, the smallest test that could catch it.

What a test actually is

A test is a small program that runs part of your app, then makes a claim about what came out. If the claim holds, the test passes silently. If it does not, the test fails and prints why.

The claim is the whole point. A test that says "calling this function did not crash" claims almost nothing. A test that says "a record 40 days past its interval is reported as overdue" claims a rule your users depend on.

This gives you the rule that decides everything today: a test asserts observable behaviour at a seam. A seam is a place where one part of your system hands something to another — a function returns a value, an HTTP route returns a status code, a page shows a label. Behaviour crossing a seam is what other code (and your users) actually depend on.

The opposite is a test that restates the implementation: it checks that a private variable is named count, or that a helper was called three times. Those tests fail every time you tidy the code, even when nothing a user can see has changed. They cost you real time and catch nothing. A test coupled to implementation is worse than no test, because it also teaches you to distrust failures.

Test points, not trace probes

When you validate a board you probe designated test points: the regulator output, the reference voltage, the signal at the connector. You do not solder a probe to an internal via and write a spec saying "this via must carry 1.8 V" — the next layout revision moves that via, the test screams, and nothing was ever wrong. Seams are test points. Implementation details are vias.

The three levels

Every automated test sits at one of three levels. They differ in how much of the system they wake up, and that single fact decides everything about their speed and their value.

Unit tests exercise one piece of logic in isolation — usually a single function — with nothing real behind it: no database, no network, no browser. Because there is nothing to start up, they run in milliseconds and you can have hundreds. They are the fastest and most isolated of the three, and when one fails you know exactly which function is wrong. Their limit: they cannot tell you whether the pieces are wired together correctly.

Integration tests exercise several real components together — typically your Express route, your service layer, and a real PostgreSQL database. They start something up, so they take seconds rather than milliseconds. They catch the bugs units cannot: a wrong SQL column name, a missing await, a route that forgets its authorization check.

End-to-end (E2E) tests drive the real browser against the real running app, the way a user does: click login, fill the form, read the page. They are the only tests that prove the whole stack works. They are also the slowest, the most fragile, and the hardest to debug — a failure tells you "the journey broke" without saying where.

Bench, subassembly, field

Unit tests are component-level bench measurements: one part, known inputs, instant reading. Integration tests are subassembly tests — the board plus its real power supply, on the bench. E2E tests are the field trial with the enclosure closed. You need all three, and the sensible ratio is obvious once stated: many bench measurements, fewer subassembly runs, a handful of field trials. That shape is the test pyramid.

Choosing the level: the smallest test that can prove it

For each behaviour, ask: what is the smallest test that would fail if this behaviour broke?

  • A pure calculation ("a record is overdue when today is past its due date") → unit. No database is involved in the rule, so involving one only makes the test slower and less precise.
  • A rule about the database or the HTTP contract ("creating equipment with a duplicate serial returns 409") → integration. The uniqueness lives in a database constraint; a unit test with a fake database would only prove your fake behaves the way you imagined.
  • A whole journey ("a technician can log in and record maintenance") → E2E. Nothing smaller covers the session cookie, the routing, and the form together.

Choosing too big wastes minutes on every run and gives vague failures. Choosing too small proves something true about a fake and nothing about your app.

High-value paths

You cannot test everything, and trying to is how people give up on testing. Rank candidates by risk, which is roughly how likely is this to break × how bad is it if it does.

High value: money and date calculations, permission checks, anything that writes to the database, anything that has already broken once. Low value: static text, styling, thin wrappers that only pass an argument along, and code you are about to rewrite anyway.

Insuring the right things

You insure the house and the car, not the cutlery. It is not that cutlery cannot be lost — it is that the loss is survivable and the premium is not worth paying. Test budget is a premium; spend it where a failure would actually hurt.

Walkthrough: one risk becomes one row

Take a real rule from your app: equipment is overdue when today is later than the last service date plus the service interval. Write it as a behaviour statement first, in plain words with a concrete example:

An item serviced on 2026-01-01 with a 30-day interval is overdue on 2026-02-05 and not overdue on 2026-01-20.

Now place it. The rule is arithmetic over two dates and a number. No database, no HTTP, no browser — so the smallest test that could prove it is a unit test. Here is what that test looks like, so the level is not an abstraction:

import { describe, it, expect } from "vitest";
import { isOverdue } from "../src/rules.js";

describe("isOverdue", () => {
  it("flags an item past its service interval", () => {
    expect(isOverdue("2026-01-01", 30, "2026-02-05")).toBe(true);
  });
});

Run it with npx vitest run and, when the rule is wrong, Vitest prints the claim and what actually happened:

 FAIL  tests/rules.test.js > isOverdue > flags an item past its service interval
AssertionError: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

Read that top to bottom: the file, the describe block, the test name, then the claim that failed. Reading test output is the skill; the syntax is the easy part. You write these for real tomorrow.

Checkpoint

For each of these, say the level out loud: "password hashing produces a different hash each time"; "GET /api/equipment without a token returns 401"; "a manager can delete equipment a technician created". (Unit; integration; integration — unless you specifically want to prove the button is visible in the UI, which is E2E.)

Your turn

Build the deliverable: a prioritized test plan tied to real risks, as a table in docs/test-plan.md inside your project.

  1. Create the file with these columns: Behaviour | Level | Risk (H/M/L) | Why this level | Covered?
  2. Calculation rules. List at least three: overdue detection, next-due-date, and the status label ("Overdue" / "Due soon" / "OK"). Write each as a behaviour statement with a concrete example, not a function name.
  3. Permissions. List at least three, including one negative case — something a user must not be able to do, such as editing another user's record.
  4. API routes. For your two busiest routes, list the success case and at least two failure cases (invalid input, missing token, duplicate value).
  5. Database operations. List one behaviour that only a real database can prove — a unique constraint, a cascade delete, or a transaction that must roll back.
  6. User journeys. List at most two end-to-end journeys. If you wrote more than two, cut until two remain; that constraint is the pyramid doing its job.
  7. Now sort the whole table by risk, highest first, and draw a line under the top ten. Everything below the line is explicitly not being tested this week. Writing that down is part of the plan.

Reviewer mode — after your table exists

Today's AI mode is reviewer: you bring finished work and ask for defects, never for a rewrite. A useful review produces specific, actionable findings with evidence pointing at your rows — not general praise.

"Review my test matrix for duplicated coverage, missing failures, and tests coupled to implementation details."

Judge every finding yourself before changing a row. Expect two real ones: a behaviour covered at two levels at once, and a missing failure case.

You are done when

Every row names an observable behaviour, a level, and a reason for that level — and you can defend why the ten above the line beat the ones below.

Common pitfalls

  • Writing the test list as function names. "Test calculateDue" says nothing about what must be true. Behaviour statements with concrete examples become tests almost mechanically.
  • Only listing success cases. Most production bugs live in the failure paths: invalid input, missing permission, duplicate value, empty list. If a row has no failure sibling, add one.
  • Reaching for E2E because it feels most real. Ten E2E tests take minutes, break for unrelated reasons, and tell you little about where the fault is. Two is a plan; ten is a maintenance job.
  • Chasing a coverage percentage. Coverage counts lines executed, not claims proven. You can hit 90% while asserting nothing meaningful.

Verify it yourself

Open today's reference, the Vitest Getting started guide, and read its first example test.

  1. Vitest's example uses test(); this lesson used it() inside describe(). Find whether the guide treats them as the same thing, and note the answer in docs/test-plan.md.
  2. Find how Vitest expects test files to be named by default. Does the pattern match the file paths you assumed in your plan? Fix your plan if not — tomorrow you run these for real.

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

    Create a test matrix for calculation rules, permissions, API routes, database operations, and user journeys.

  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

A prioritized test plan tied to real risks.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Review my test matrix for duplicated coverage, missing failures, and tests coupled to implementation details.

References

End-of-day quiz

Q1 Which tests are usually fastest and most isolated?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.