‹ Build a Screener Lesson 13 of 17
Contents Lesson 13 of 17

6 min read · professional

The review that runs itself

The four lenses work, and they have one flaw: you have to remember to run them. This lesson turns the parts you can state as a rule into a check that runs without you, and it is the first time in this track that you write a test.

A test is a function that complains

There is no ceremony to it. A test calls your code with a known input and says what the answer should be. If the answer differs, it complains and names the difference.

Your screener already has the function from the last lesson, the one that turned 0.0002 into 0.02%:

// src/lib/format.ts
export function asPercent(fraction: number): string {
  return `${(fraction * 100).toFixed(2)}%`;
}

The test goes beside it, and the name of the file is what makes the runner find it:

// src/lib/format.test.ts
import { expect, test } from "vitest";
import { asPercent } from "./format";

test("a yield of 0.0002 renders as 0.02 per cent", () => {
  expect(asPercent(0.0002)).toBe("0.02%");
});

Install the runner once, then run it:

npm install -D vitest
npx vitest run
 ✓ src/lib/format.test.ts (1 test) 3ms
 Test Files  1 passed (1)

That is the whole apparatus. One import, one test, one expect.

Make it fail before you believe it

A test that has never failed proves nothing, because a test can pass by not running at all. Change the expected string to "0.03%" and run it again:

 × a yield of 0.0002 renders as 0.02 per cent
   → expected '0.02%' to be '0.03%'

Now you have seen it work. Put the 0.02% back. Do this the first time you write a test in any project, and every time you write one for a bug: watch it go red on the broken code, then green on the fix. Otherwise you have written a comment that takes longer to run.

Write one for each bug the review found

The three lessons before this one found real defects in your screener. Each of them is one test, and each takes about a minute:

test("a missing dividend yield is a dash, not a zero", () => {
  expect(cell(undefined)).toBe("—");
});

test("limit is clamped no matter what the caller asks for", () => {
  expect(clampLimit("500")).toBe(100);   // the cap
  expect(clampLimit("abc")).toBe(25);    // the default
});

Those two literals come from the clamp you wrote in unit 2 lesson 3 — Math.min(Math.max(Number(raw) || 25, 1), 100) — worked out by hand rather than copied from what it printed. Different answers, because the two inputs take different paths: "500" is a number above the cap, "abc" is not a number at all and falls to the default. A test asserting the same value for both would pass against a clamp that had lost one of those branches.

Note what the second one asserts. clampLimit takes a string, because that is what a query parameter is, and "abc" is what a real caller will eventually send. The review lesson said an assistant is reliably strong on the happy path and reliably weak on the boundary — so the boundary is what you spend a test on.

This is the difference the unit has been building towards. A review finds a bug once. A test finds it every time anyone touches that file, including in six weeks, including when it is your assistant touching it and not you.

What not to test

The suite is worth having only if it stays small enough that a failure means something.

  • Not the framework. Array.sort sorts. toFixed rounds. Testing those tests somebody else's code.
  • Not the mock. If your test replaces the fetch with a function that returns two hard-coded rows and then asserts there are two rows, it will pass forever and tells you nothing about your screener.
  • Not the layout. That a column is called "Yield" is not worth a test; that its numbers are in per cent is.

The rule that decides it: test the thing that would be wrong quietly. A broken layout is loud. A yield that is a hundred times too small is silent, ranks your shortlist, and looks fine.

Ask your assistant for the test, then read it like any other diff

Assistants write tests quickly and they have one characteristic failure worth knowing.

They will write a test that mirrors the implementation rather than the requirement:

// Passes forever, including when both are wrong.
expect(asPercent(x)).toBe(`${(x * 100).toFixed(2)}%`);

That test computes the answer the same way the code does, so it agrees with the code no matter what the code says. If the multiplier is wrong, the test is wrong in exactly the same direction and stays green. Assert a literal you worked out yourself — "0.02%" — and the test has an opinion of its own.

Everything else about reading a generated test is the loop from course 1. It is a diff. Read it.

Try it now

Install vitest, write the asPercent test, and break it on purpose so you see it go red. Then write one test for each defect you found in the last three lessons — leaks, cost, lies — and commit them with the fixes. Count them: three or four tests, written in ten minutes, are the ones that will still be checking your screener when you have forgotten the review that produced them.