Skip to content

Testing

A test is worth exactly what it can disprove. Suites in which nothing can go red report success at any cost, but that is the one report nobody should act on. Tests run on Vitest and live next to the code they cover.

Tests earn their place by what they disprove

Section titled “Tests earn their place by what they disprove”

A passing test proves nothing on its own, because a test that cannot fail passes for free. Ten tests that go red for a realistic defect are worth more than a thousand that go green no matter what the code does. And the thousand cost more to run and far more to maintain.

  • Justify every test by what it can disprove, weighed against the maintenance it will demand for the rest of its life.
  • Delete a test that cannot fail for a realistic defect. It burns runtime and review attention and hands back a green light nobody should have trusted.

A test states one behaviour of the module, observed through the public contract. Internals are the implementation’s business. A test that reaches into them turns every refactor into a rewrite of the suite, and the suite ends up voting against the change it was meant to protect.

  • Write the name as a behaviour statement: subject, behaviour, condition. execute reports a timeout when the entry never settles reads as a fact about the module.
  • Write the failing test before the implementation. The failing test is the specification, and watching it fail for the expected reason is the only proof that it can fail at all.
  • Skip the scenario ceremony. Given-when-then syntax and scenario objects wrap vocabulary around what a plain test already said, and the wrapper is the part that has to be maintained.

A test walks one journey from end to end. Build the subject once, drive every step that journey takes, and check every outcome it produces on the way. Setup is the expensive part, so cutting a journey into fragments pays that cost again for each fragment and proves nothing the whole journey did not already prove.

  • Let one test carry every assertion its journey produces. A test that checks the result, the logs and the disposal of a single execution has stated that behaviour completely.
  • Keep an assertion where its subject lives. If it reads the same envelope, worker or client the previous line asserted on, it belongs in that test.
  • Shorten the suite by merging tests, never by deleting assertions. Strip the assertions and what is left runs faster. But it proves nothing.

A behaviour belongs at the cheapest layer that can still show it broken. Each heavier layer buys realism with runtime and with flakiness, so it has to be earned and never assumed.

Layer Fits when Wrong when
Pure-logic spec, no isolate The behaviour is a transformation, a parse, a name sanitization, queue accounting or a limit decision The behaviour only appears once real code runs across the isolate boundary; promote it to an isolate spec
Isolate spec, real isolated-vm The behaviour depends on compilation, module linking, memory limits, timers or the host bridge inside a real isolate Nothing crosses the isolate boundary; a pure-logic spec falsifies the same behaviour in milliseconds
Pool spec, real forked child process The behaviour depends on the process boundary: supervision, crash handling, IPC transport, lifecycle and backpressure The behaviour survives inside one process; drop to a lighter spec and leave the pool spec covering the boundary itself
Testbed suite, a real workerd or a real daemon The behaviour is part of the contract with the upstream package across the supported version matrix, belongs to the execute path a migrating host walks, or holds only when the client runs in another runtime Anything provable inside the executor package; each testbed suite stays small and grows only when the contract it pins does

Run one project’s specs with pnpm nx test executor, and the testbed suites with pnpm nx test executor-testbed. The test lane of the gate runs every project through pnpm test; see Tooling.

  • Never test what the types already rule out. A test that a function rejects a number where the signature says string re-proves the compiler.
  • Never test prose no caller depends on. A description, a hint or a warning can be reworded without breaking anyone, so it is not a contract and a test on it fails on the day someone improves the wording.
  • Never test an unreachable edge case. A defensive branch reachable only by editing the module has no realistic defect to falsify.
  • Never test a defect that cannot come back. Cover it only when the surrounding path deserves coverage on its own merit.

But some strings are not prose at all: the ones shared with @cloudflare/codemode are public contract, because a caller migrating off the upstream executor may be matching on them. Those get pinned exactly, character for character and never by substring, because rewording one breaks somebody.

Deleting and consolidating low-signal tests is engineering, done by hand and with the same care as writing them. A suite either keeps earning the minutes it costs or it stops being worth having.

A mutation probe measures falsifying power directly. Flip a >= to a >, or add one to a limit, or delete a cleanup call, then run the suite. If it stays green, it has just named the assertion nobody wrote. Revert the probe once it has answered, because a probe is a diagnostic and never a change.

Each source file has one spec file: the same name plus .spec.ts, in the same directory. isolate.ts is covered by isolate.spec.ts and by nothing else, so consolidation happens inside that one file, never by adding a second file for the same module.

  • Keep spec files flat, with top-level test() calls. Reach for describe only where the grouping carries real meaning, such as separating two named exports of one module.
  • Use test(), never it(); the lint lane fails otherwise. Blank lines between test blocks are enforced there too; see Code style.
  • Use the .spec.ts suffix. .test.ts is banned, and the gate scans for it because no lane reports one; Tooling explains what a stray one costs.

When a few cases need a mocked dependency and the rest of the module’s tests need the real one, both stay in the one spec file. Use vi.resetModules, then vi.doMock, then a dynamic import of the module under test inside the test that needs the mock.

import { expect, test, vi } from 'vitest'
test('reports a failed isolate probe as an internal error', async () => {
vi.resetModules()
vi.doMock('isolated-vm', () => {
class Isolate {
constructor() {
throw new Error('addon not loadable')
}
}
return { default: { Isolate } }
})
const { assertEngineServesSandbox } = await import('./isolate.js')
await expect(assertEngineServesSandbox()).rejects.toMatchObject({
name: 'ExecutorError',
code: 'INTERNAL',
})
})

The reset gives the dynamically imported module its own copy of every class it touches, so a class identity from the outer import graph never matches one from the inner. Assert on name and code, never with instanceof. The same property holds across process and RPC boundaries, so typed errors travel by code; see Design.

Every spec runs offline and deterministically. A test that reaches the network, races a real clock, or leans on an ordering the runtime never promised reports failures that have nothing to do with the code under review. Suites reporting those failures get rerun until they are green, and nobody reads them.

Production code may carry a branch that exists only for tests. Injecting a failure into the real path proves what a lookalike fixture cannot. So such a branch is designed and not tolerated. The shape is always the same: ask whether the process is running under test, then act. One repository-wide helper answers that question, so a class never reads the environment itself and never stores a test flag as a property.

if (inTestEnvironment() && request.code === ABORT_CODE) {
process.abort()
}

Behind that guard a plain marker in the data is fine. Words that read as test vocabulary, stub first among them, stay out of production identifiers; see Naming.

Compatibility with the upstream package is proven by running both systems end to end, never by pinning tests to upstream internals. The compatibility suite replays one set of cases through the reference executor of every supported upstream line and through this executor, normalizes both sides, and diffs them. It runs as a required CI job, so an upstream release arrives as a diff, not as a question.

  • Prove the declared range with the matrix, so that the range in peerDependencies is a tested fact and not an assertion. Dependencies describes how the lines are installed side by side.
  • Normalize before diffing. Results go through a JSON round trip so a Map or a Set collapses the same way on both sides, and timing never takes part in a diff.
  • Keep the suite small. It covers the contract, not the executor’s internals, and every case in it is paid for again on every supported line.