"Tests should be isolated" is stated so often it stops meaning anything specific. Gerard Meszaros's xUnit Test Patterns gives the idea exact, checkable vocabulary, built around one core tension: every test needs a fixture — everything required to exercise the system under test, including its data and dependencies — and how you build that fixture, fresh per test or shared across many, is the single decision that determines whether your tests can interfere with each other at all.
Fresh Fixture: A New One, Every Time
A Fresh Fixture means each test constructs its own private fixture from scratch, used by that test alone:
test('removes an item from the cart', async () => {
const cart = new Cart(); // fresh, private to this test
cart.addItem({ id: 'backpack', price: 29.99 });
cart.removeItem('backpack');
expect(cart.items).toHaveLength(0);
});
test('calculates cart total with tax', async () => {
const cart = new Cart(); // a DIFFERENT fresh Cart, no relation to the one above
cart.addItem({ id: 'backpack', price: 29.99 });
expect(cart.getTotalWithTax(0.08)).toBeCloseTo(32.39);
});
Neither test can possibly affect the other's outcome, because neither one's Cart object exists anywhere the other test can reach it. This is the strongest form of isolation Meszaros's vocabulary describes, and it's the default worth reaching for unless you have a specific, deliberate reason not to.
Shared Fixture: One Instance, Reused
A Shared Fixture is the opposite: one fixture instance, built once, reused across many tests — usually adopted for speed, since constructing the fixture (a database connection, a logged-in browser session, a seeded set of test data) can be genuinely expensive to redo for every single test.
let sharedCart: Cart; // one instance, shared across everything below
beforeAll(() => {
sharedCart = new Cart();
sharedCart.addItem({ id: 'backpack', price: 29.99 });
});
test('removes an item from the cart', async () => {
sharedCart.removeItem('backpack'); // mutates the SHARED instance
expect(sharedCart.items).toHaveLength(0);
});
test('calculates cart total with tax', async () => {
// this test now runs against a cart with ZERO items, because the
// previous test already removed the one item — if it runs after
expect(sharedCart.getTotalWithTax(0.08)).toBeCloseTo(32.39); // FAILS
});
The second test's correctness now depends on execution order — something xUnit Test Patterns names explicitly as the cause of an Erratic Test: a test that passes or fails inconsistently, not because the code under test is actually broken, but because a shared fixture was left in a different state by whichever test happened to run before it. Reverse the order these two tests run in, and the second one passes while the first one now might not — the tests haven't changed at all; only which one mutated the shared state first has.
Why This Is the Real Root Cause Behind "My Tests Pass Alone but Fail in the Full Suite"
That specific symptom — a test that's reliably green when run by itself, and unreliably red as part of a larger run — is close to a textbook diagnostic for an Erratic Test caused by an unintentionally Shared Fixture. Run alone, there's no other test to have mutated the shared state first, so the fixture is always in its expected starting condition. Run as part of the full suite, some other test — possibly one you've never even looked at, possibly one added long after this test was written — reaches the same shared fixture first and leaves it in a state this test never accounted for.
This is precisely why parallel test execution, covered elsewhere on this blog and in this tutorial's own chapters, makes isolation problems dramatically worse rather than just "sometimes worse": with tests running concurrently across workers, you don't even get the comfort of one deterministic execution order to (incorrectly) rely on — the actual order two tests touch a shared resource can differ from run to run, which turns a bug that would at least be consistently wrong into one that's inconsistently wrong, the single hardest kind to track down by inspection.
The Middle Ground: Immutable Shared Fixture
Meszaros's own resolution to the speed-versus-safety tradeoff isn't "always use Fresh Fixture, no matter the cost" — it's a specific compromise worth knowing by name: partition a shared fixture into the parts every test needs but never modifies, and the parts individual tests do need to change, building only the mutable part fresh per test while still reusing the expensive, unchanging part:
let sharedProductCatalog: Product[]; // expensive to build, but NO test ever mutates it
beforeAll(async () => {
sharedProductCatalog = await seedProductCatalog(); // built once, genuinely safe to share
});
test('removes an item from the cart', async () => {
const cart = new Cart(sharedProductCatalog); // fresh Cart, reusing the shared (read-only) catalog
cart.addItem(sharedProductCatalog[0]);
cart.removeItem(sharedProductCatalog[0].id);
expect(cart.items).toHaveLength(0);
});
sharedProductCatalog is read from, never written to, by any test — so sharing it carries none of the risk a mutable shared Cart carried above, while still avoiding the cost of reseeding the entire catalog for every single test.
The Actual Decision Worth Making Explicitly
The question worth asking about any fixture, before defaulting to whichever pattern is fastest to type, is precise: will any test that uses this fixture mutate it? If yes, that fixture needs to be fresh per test, full stop — sharing a mutable fixture isn't a performance optimization with a small downside, it's a guaranteed source of Erratic Tests waiting for the specific execution order that exposes it, which might not happen until months after the fixture was first shared, whenever the test suite's order or parallelism setup happens to change. If no — the fixture is read-only for every test that touches it — sharing it is free, safe, and exactly the case Meszaros's Immutable Shared Fixture pattern exists to name and recommend.
