The failing test is the victim
An assertion that holds in isolation and breaks in company is telling you something precise: the code under test depends on state that another test changed. The test that fails is almost never the test that is wrong, so reading it more carefully is the least productive thing you can do. What you need is the pair — the test that left something behind and the test that noticed.
Say so before you start debugging, because it reframes the work from "why is this assertion wrong" to "what do these two tests share". Everything shared between two test cases in a Node process is a candidate: a module's top-level state, process.env, the global clock, the fetch implementation, the filesystem, and the database.
Modules evaluate once and their exports are shared
The mechanism people underestimate is that a module body runs once per module registry and its exports are then the same objects for every test that imports them. A test runner typically gives each test file a fresh registry — Jest does, and it also runs files in separate worker processes by default — but every test within one file shares it.
import { config } from './config.js';
test('disables retries on the batch path', () => {
config.retries = 0;
expect(runBatch()).toEqual({ ok: true });
});
test('retries a flaky upstream three times', () => {
expect(callUpstream()).toMatchObject({ attempts: 3 });
});
Trace it with the initial value 3. Run the second test with -t and its own name and config.retries is 3, the upstream is called three times, green. Run the file and the first test sets it to 0 before the second one reads it, so attempts is 0 and the assertion fails with a message that points at the retry logic, which is entirely innocent.
The same shape appears with anything captured at import time rather than read per call. A module that reads process.env.FEATURE_X into a constant at the top level cannot be influenced by a test that sets the variable in beforeEach, because the read already happened. That one is doubly confusing because it fails in isolation too, just for a different reason.
Globals patched and not restored
The other reliable source is a stub that outlives its test. Fake timers installed and not uninstalled leave every subsequent test with a clock that does not advance, so a test that awaits a debounce hangs until the runner's timeout and reports as slow rather than as poisoned. A replaced global.fetch, a spy on console, a monkeypatched Date.now, an added listener on process — each is invisible in the test that installed it and lethal to a test that runs later.
The structural answer is that installation and restoration must be symmetrical and enforced by the framework rather than by discipline: create stubs in beforeEach, restore them in afterEach, and turn on the runner's restore-mocks setting so a forgotten teardown cannot silently persist. Where the shared thing is a database, an outer transaction rolled back after each test is far more reliable than a truncate-what-I-touched cleanup, because rollback does not depend on the test having listed everything it wrote.
Bisect the ordering rather than reasoning about it
You will not find the pair by reading, because the offending test looks perfectly reasonable. Halve the problem instead. Run the failing test with a prefix of the suite, keep halving the prefix, and you converge on the culprit in a handful of runs. Two flags make that tractable: run in band so worker scheduling stops adding noise, and pass explicit paths so you control the order exactly rather than inheriting the runner's file discovery.
Once you have the pair, the remaining question is which of them is at fault, and the answer is usually the writer rather than the reader. A test that mutates shared state is wrong even if every current test tolerates it, because it makes the suite's result depend on file naming.
Order dependence versus a genuine race
These two present identically as intermittent failures and need opposite fixes, so separate them early. Order dependence is deterministic once you know the order: the same sequence fails every time. A race between parallel workers is not — the same command fails one run in twenty, and running serially makes it disappear. If serialising fixes it, you have contention on something outside the process, typically a fixed database name, a hard-coded port, or a temporary directory path shared by every worker. The fix there is to make the resource per-worker rather than to serialise, because serialising trades a real signal for wall-clock time you will resent later.
Retrying is a decision to ship the bug
The pressure to add a retry, or to mark the test skipped with a ticket nobody will pick up, is at its highest exactly when the suite is red and a release is waiting. It is worth naming the cost out loud. A test that passes on the second attempt has told you your production code or your test harness has a state leak, and the leak does not know it is only allowed to manifest in CI. Order dependence in tests very often mirrors real shared mutable state in the application — a module-level cache, a connection pool configured once, a client whose retry count is global — and a suite configured to hide it has removed your only detector.
The stronger habit is to run the suite in a randomised order deliberately, so that order dependence fails on the day it is introduced, by the person who introduced it, instead of six months later on someone else's unrelated change.
A test that only fails in company is reporting shared mutable state, so the work is to bisect the ordering until you have the pair and then remove the sharing, rather than to make the assertion tolerant of it.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).