General Fixture.
A shared setup builds one big "kitchen-sink" fixture covering every test's needs, but each individual test exercises only a small slice of it.
##Signs and Symptoms
A single beforeEach/setUp (or shared module-level setup) constructs lots of objects, seeds many records, and wires up several mocks — and any given test touches only one or two of them. The detection heuristic from the test-smell catalog is simply: not all fields created in setup are used by all test methods.
How to recognize it:
- The setup block is long and grows every time a new test is added.
- To understand a single test you must scroll up and reverse-engineer which parts of the fixture actually matter to it.
- Many tests fail or need editing when you change one shared object that most of them don't even care about.
- The same fixture serves wildly different scenarios (creation, validation, payment, permissions) from one place.
// Smell: one fixture for everything; each test uses a sliver of it
let user, admin, product, cart, order, paymentGateway, shippingZone;
beforeEach(() => {
user = createUser({ name: 'Ada' });
admin = createAdmin();
product = createProduct({ price: 10 });
cart = createCart(user, [product]);
order = createOrder(cart);
paymentGateway = mockGateway();
shippingZone = createZone('EU');
});
test('cart subtotal sums its line items', () => {
// only needs cart + product, yet pays for admin, order, gateway, zone…
expect(cart.subtotal()).toBe(10);
});
##Reasons for the Problem
Why it happens
- DRY applied too aggressively. Setup is consolidated into one shared hook to "avoid duplication," so every new test's needs get bolted onto the same fixture.
- Organic growth. The fixture accretes objects as tests are added, and nobody prunes what older tests no longer use.
- Implicit setup inertia. Once a big
beforeEachexists, adding one more line is easier than creating a focused fixture for the new case.
Why it hurts
- Readability / Tests-as-Documentation. A test should read as a clear cause→effect. When most of the fixture is irrelevant noise, the reader can't tell what actually drives the result. Meszaros' antidote, the Minimal Fixture, exists precisely because a test using the smallest fixture is always easier to understand.
- Maintainability. A change demanded by one test (e.g. give
producta new required field) forces edits to setup shared by all tests, creating ripple-effect breakage and coupling unrelated tests together. - Reliability. Shared, mutable fixture state lets tests influence each other and makes failures hard to localize — a classic source of order-dependent flakiness.
- Speed. Every test pays the full cost of building the whole fixture. General Fixture is listed among the causes of Slow Tests.
- False confidence. Tests become over-specified against incidental fixture details, so they pass or fail for reasons unrelated to the behavior under test.
##Treatment
Aim for a Minimal Fixture: each test sets up only what that test needs, and nothing more.
- Inventory usage. For each shared field, list which tests actually read it. Anything used by only a subset is a candidate to move out of the shared setup.
- Push construction into Creation Methods / test-data builders (Delegated Setup). Keep tests concise without forcing one giant fixture — each test calls a builder and overrides only the attributes relevant to it.
- Prefer a Fresh Fixture per test over a long-lived shared one, eliminating cross-test coupling and mutable shared state.
- Split by scenario. If groups of tests genuinely share a small fixture, separate them into focused
describeblocks (or test classes), each with its own minimal setup. - Keep in
beforeEachonly what is truly common and small; move the rest inline so cause and effect sit next to the assertion.
// Before: general fixture in a shared hook (see Signs & Symptoms)
// After: minimal, delegated setup — each test builds only what it needs
test('cart subtotal sums its line items', () => {
const cart = aCart().withItem(aProduct().price(10)).build();
expect(cart.subtotal()).toBe(10);
});
test('checkout charges the payment gateway', () => {
const gateway = mockGateway();
const cart = aCart().withItem(aProduct().price(10)).build();
checkout(cart, gateway);
expect(gateway.charge).toHaveBeenCalledWith(10);
});
Each test now reads top-to-bottom as a self-contained story, builders absorb the boilerplate, and changing one scenario no longer disturbs the others.