Testing foundations

Make tests deterministic

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

8 minute lesson

~~~

A flaky test sometimes passes and sometimes fails without a product change. It trains the team to ignore the suite.

Inject clocks and ID generators, use isolated data, wait for observable conditions instead of sleeping, and restore modified globals after each case. Parallel execution is safe only when tests do not share mutable state.

Think of determinism as explicit inputs plus controlled dependencies. If a result depends on the wall clock, random UUIDs, process environment, test order, or a row left by another test, you have hidden inputs.

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')

This test can assert exact output without guessing an acceptable time range. A range such as “created within the last second” can pass with the wrong timestamp and fail on a busy CI runner. Likewise, sleeping for 100 milliseconds does not synchronize anything; it only hopes the other work finishes first.

Control randomness with an injected generator or a recorded seed. Give parallel tests unique files, ports, schemas, or record identifiers. Register cleanup as soon as a resource is created so it still runs after an assertion fails. If you replace a global, restore it in teardown; otherwise a passing test can poison the next one.

Change book creation to accept an injected clock and ID generator, then assert exact output without patching global time. Run the test repeatedly and in parallel with another case that uses different fixed values.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →