ConstructiCat Logo
CodeBust.
Browse section ▾

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 beforeEach exists, 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 product a 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.

  1. 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.
  2. 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.
  3. Prefer a Fresh Fixture per test over a long-lived shared one, eliminating cross-test coupling and mutable shared state.
  4. Split by scenario. If groups of tests genuinely share a small fixture, separate them into focused describe blocks (or test classes), each with its own minimal setup.
  5. Keep in beforeEach only 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.