Testing foundations

Make tests deterministic

Control time, randomness, data, environment, and cleanup so the same code produces the same result on every run.

A flaky test passes sometimes and fails sometimes, with no change to the code. One flaky test is enough to train a team to hit “rerun” without reading the failure. Then a real failure slips through.

A test is deterministic when the same code gives the same result on every run. The way to get there is to make every input explicit.

Find the hidden inputs

If the result depends on the wall clock, a random UUID, an environment variable, the order tests run in, or a row left behind by another test, your test has hidden inputs. You didn’t write them down, but they change the outcome.

Take createBook(). It stamps each book with an ID and a creation time. Let’s pass both in instead of letting the function grab them:

const book = createBook(
  { title: 'Dune', author: 'Frank Herbert' },
  {
    now: () => new Date('2026-01-15T10:00:00Z'),
    createId: () => 'book-123'
  }
)

assert.equal(book.id, 'book-123')
assert.equal(book.createdAt, '2026-01-15T10:00:00.000Z')

Now we can assert the exact output. No guessing an acceptable range.

Compare that with the alternative: “created within the last second”. That check passes with the wrong timestamp, and it fails on a busy CI runner that takes two seconds. It tests the machine, not the code.

Don’t sleep, wait

The same goes for setTimeout in tests. Sleeping 100 milliseconds doesn’t synchronize anything. It hopes the other work finishes first. On your laptop it does. On CI, sometimes it doesn’t.

Wait for the condition you actually need: the row exists, the promise resolved, the element is visible. We’ll see how Playwright does this for browser tests later in the course.

Isolate and clean up

Randomness: inject the generator, or record the seed so you can replay a failure.

Shared state: give parallel tests their own files, ports, schemas, or record IDs. Parallel execution is only safe when nothing is shared.

Cleanup: register it the moment you create a resource, not at the end of the test. If an assertion throws halfway through, cleanup at the bottom never runs, and the leftover file breaks the next test.

Globals: if you replace Date.now or Math.random, restore it in teardown. A passing test that forgets to restore a global poisons every test after it.

Try this on the Books API: change book creation to accept an injected clock and ID generator, then assert the exact output. Run the test ten times in a row, and in parallel with another case that uses different fixed values. It should pass every time.

Lesson completed