Skip to content
Preptima
mediumCodingConceptMidSenior

A test passes on its own and fails when the whole suite runs. How do you track that down?

The failing test is a victim, not the cause: something earlier mutated state it shares, usually a module evaluated once, a global that was patched and not restored, or rows left in a database. Bisect the ordering to find the pair, then remove the sharing.

5 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate bisect the ordering to isolate a pair rather than re-reading the failing test
  • Whether module-level evaluation is named as mutable state that one test can change and never restore
  • That randomised ordering is proposed so order dependence fails immediately instead of intermittently
  • Whether a retry or a skip is described as a decision to ship the bug rather than as a fix
  • Does the answer separate order dependence from a genuine race between parallel workers

Answer

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'; // one object, shared by every test in this file

test('disables retries on the batch path', () => {
  config.retries = 0; // <-- mutated and never restored
  expect(runBatch()).toEqual({ ok: true });
});

test('retries a flaky upstream three times', () => {
  // Passes alone, where config.retries is still its initial 3.
  expect(callUpstream()).toMatchObject({ attempts: 3 }); // sees 0, fails
});

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.

Likely follow-ups

  • Two workers run in parallel against one Postgres instance. What do you change, and what do you refuse to change?
  • The suite finishes but the process never exits. What is still open, and how do you find it?
  • How do you test code that reads the clock and has a 30-second timeout without the suite taking 30 seconds?
  • When is running serially the right permanent answer rather than a diagnostic step?

Related questions

Further reading

testingtest-isolationflaky-testsjestnodejs