Skip to content

Cache test results across machines

Updated

Test runners on their own don't reuse a pass: their caches make the next run faster, but the tests still run. To reuse a result on another machine you need a build tool that models your tests, or a wrapper that recognizes the code.

ToolWhat it reusesAcross machinesWhat it asks of you
VitestNothing: --changed picks tests, and its cache orders themNoNothing
JestNothing: --onlyChanged picks testsNoA git or Mercurial repository
GradleA Test task's outputs, with --build-cacheWith a remote build cacheA Gradle build
BazelTest resultsWith a remote cacheBUILD files for each package
NxTask resultsWith a remote cacheAn Nx workspace
TurborepoTask resultsWith a remote cacheturbo.json and package.json scripts
hushrunA check's passWith a shared backendOne wrapper around the command

Test runners pick tests

Vitest's --changed runs the tests affected by changed files, and Jest's --onlyChanged does the same from version control. Both help while you work. They still need something to compare against, and neither records that a run passed. Vitest's cache puts failed and slow files first.

Build tools reuse results

A build tool that knows a test's inputs can decide it needn't run.

These are precise: change one package, and only what depends on it runs. The cost is adopting the tool, and describing your project to it.

A wrapper reuses a pass

hushrun run needs no description of your project. It wraps the command you already have, in any language:

package.json
"scripts": {
  "test": "hushrun run -- vitest run"
}
hushrun run unit -- cargo test

Its key is the repository's files, the check's name, the exact command and the directory. Results go to a backend your laptops, CI and coding agents share, so a pass in one place skips the check in the others.

The trade is precision. By default a change anywhere in the repository runs every check again, until you split it into scopes. It also restores no outputs: a skip means the command didn't run, so a coverage report or a test log isn't there. Run with --force when you need them.

What makes a cached result wrong

Every one of these tools can reuse a result it shouldn't when a test depends on something outside its key:

  • Environment variables and secrets. Nx, Gradle and Turborepo count them when you declare them, apart from framework prefixes Turborepo infers, such as NEXT_PUBLIC_*. hushrun doesn't count them.
  • The runtime or operating system. Nx hashes the OS and CPU. For hushrun, give each variant its own name, such as unit-node22 and unit-node24.
  • Services and the network. A test that talks to a real database depends on that database. Keep such a test out of the cache, or expect its pass to mean less.
  • Partial runs. vitest run src/cart.test.ts isn't vitest run. hushrun treats them as two checks, so a partial pass never stands in for a full one.
Markdown