skip to content

How do you remove duplicated setup from a large test suite without creating a mystery guest?

level: seniorimportance: should knowfreq 41%

answer

  1. The obvious cure is the opposite smell
  2. Why is this the expected value?
  3. Extract construction, not meaning
  4. Default the rest, override what you assert
  5. Tests want DAMP more than DRY

basics

~20 s

Extract construction, not meaning: a builder or object mother with defaults lets each test override only the value it asserts on, keeping that value visible. A mystery guest is a test whose expectation depends on unseen data.

solid answer

~50 s

Duplicated setup and the mystery guest are opposite smells, and the naive cure for one creates the other: moving repeated fixture construction into a shared base class or a suite-wide seed file removes the duplication and hides why the expectation holds. The discipline that resolves it is to make the data a test depends on visible in the test and default everything else. In practice that means a builder with sensible defaults and per-field overrides, or an object mother exposing named canonical cases, where each test overrides exactly the field it asserts on and derives its expectation from that same value. Prefer helpers called from the test over inherited setup, since a call is traceable and a hook is not, and avoid one general fixture built for every test. Shared cost is fine; shared, invisible meaning is not.

code

pseudocode · 5 lines
pseudocode
// base class loads the 118-row shared seed for every test
test "senior maths resolves to its allocated room":
    slot = planner.publish(term).slotFor("senior-maths", TUESDAY)
    assertEquals("B-14", slot.room)
    // why B-14? only the seed file knows, and it was regenerated

go deeper

for a junior

Be ready to name the mystery guest: a test whose expected value depends on data you cannot see in the test. Knowing that a builder with defaults lets a test show only the field it cares about is enough at this level.

for a middle

Explain the mechanics of a builder and an object mother, why the asserted value must appear in the test body, and why a helper called from the test is easier to follow than setup inherited from a base class.

for a senior

Show you have lived the trade: describe a stale expectation that kept passing against regenerated shared data, when a shared fixture is still correct, and how you would sequence a migration across a large suite without stopping feature work.

for a principal

Own the standard: what the team's fixture vocabulary is allowed to become, how you keep DAMP from turning into copy-paste, and how you measure whether failures are legible rather than counting duplicated lines.

### Two smells that push in opposite directions **Duplicated setup** is the same fixture construction copied across many tests: sixty-two tests in a school timetable planner each building the same eighteen-line term calendar before they get to the behaviour they care about. It costs you on every constructor change, it buries the one line that actually differs between two tests, and it trains people to copy the nearest test rather than write the one they need. **Mystery guest** is Meszaros' name for the opposite failure: a test whose outcome depends on data that is not visible in the test. The expected value is `31`, or room `B-14`, and nothing in the test body explains why. The data lives in a suite-wide seed file, a base-class setup method, or a shared data file loaded at start-up. The reader must open two other files to understand one failure. The trap is that the obvious cure for the first smell is the second smell. "We keep rebuilding the term calendar — let's set it up once in a shared base class." Now nothing is duplicated and nothing is legible. ### What that trade actually costs In the timetable suite the shared seed held one hundred eighteen rows. A test asserted that the Tuesday senior-maths slot resolved to room `B-14`, matching a row in that seed. The seed was regenerated, the room identifiers shifted by one position, and the test kept passing — it was asserting a literal that no longer meant what its author intended. The mis-mapping it was supposed to guard reached the publish endpoint, which peaks around 1,200 requests per minute at term changeover, and corrupted rooms went out silently. A mystery guest does not only slow down diagnosis; it lets an expectation **decouple from its meaning** without anyone noticing, because nobody reads the test and the seed side by side. ### The discipline that resolves the tension One rule carries most of the weight: **make the data the test depends on visible in the test, and default everything else.** Concretely, that is a **builder** with sensible defaults and per-field overrides, or an **object mother** exposing named canonical cases — `aTermWithTwoClashingSlots()`, `aFullyBookedRoom()`. Each test calls the builder, overrides *exactly the field it asserts on*, and asserts against that same value. Construction is shared; meaning is local. The eighteen lines collapse to one, and the field that matters stays in front of the reader's eyes. Supporting rules: - **If a value appears in an assertion, it must appear in the test body.** Derive expectations from the builder's own inputs rather than hard-coding literals that mirror data defined elsewhere. - **Extract construction, not meaning.** A helper that returns a valid term calendar is good; a helper that also decides what the test expects is a mystery guest with a nicer name. - **Prefer helpers called from the test over setup inherited from a base class.** A call is visible and traceable at the call site; an inherited hook is invisible, and deep fixture hierarchies are the single most common way this smell becomes permanent. - **Avoid the general fixture** — one large object graph built for every test because some test needs each part of it. It is slow, it is a mystery guest for every test that uses one field of it, and it makes each test's real dependencies unstatable. ### When a shared fixture is still the right answer Senior judgement here is not "always inline". Some setup is genuinely expensive and genuinely immutable — a schema built once, a reference dataset, a warmed dependency — and rebuilding it per test buys nothing. Keep those, keep them read-only, and pay the cost in the *expectation*: tests should assert against values they derive or state, not against literals that silently mirror rows in the shared data. The mystery guest is created by invisible **meaning**, not by shared **cost**. Watch also for the over-correction. A fixture DSL so abstract that reading a test requires learning a private vocabulary is a mystery guest with better manners. The honest measure is behavioural: can somebody who did not write the test diagnose its failure from the test body alone? If not, whatever you extracted, you extracted too much of it. ### Doing this to an existing suite Two hundred fourteen tests do not get rewritten in a sprint, and proposing that is a weak answer. The workable sequence: add the builder; require it for new tests; convert the surrounding tests whenever you touch a file for another reason; keep a count of remaining consumers of the shared seed so the migration is visible; delete the seed when the count reaches zero. Convert first the tests that fail most often, because that is where diagnosis cost is actually paid. Finally, the framing worth saying out loud: the goal for test code is not DRY, it is DAMP — descriptive and meaningful phrases. Tests are read under pressure, at the moment something is broken, by someone who did not write them. A little repetition that keeps a failure legible is a good trade; an abstraction that hides why an expectation holds is not.

  • When is a shared fixture still the right call?
    When the setup is genuinely expensive and immutable — a schema built once, a reference dataset, a warmed dependency — rebuilding it per test buys nothing. Keep it, keep it read-only, and pay the cost in the expectation instead: tests should assert against values they state or derive, not against literals that silently mirror rows in the shared data. Shared cost is not the smell; shared, invisible meaning is.
  • What is the failure mode of over-extracting fixtures?
    You get a private fixture vocabulary so abstract that reading a test means learning a small language, which is a mystery guest with better manners. The honest measure is behavioural: can someone who did not write the test diagnose its failure from the test body alone? If not, the extraction went too far, however little duplication remains.
  • How would you migrate a suite of a couple of hundred tests off a shared seed file?
    Incrementally. Add the builder, require it for new tests, convert surrounding tests whenever you touch a file for another reason, and track the number of remaining consumers of the seed so the migration is visible. Convert the most frequently failing tests first, since that is where diagnosis cost is actually paid, and delete the seed when the count reaches zero.

A shared seed file is a recipe that says 'use the sauce from the fridge'. It saves a step until the day someone refills the jar with something else and the dish still passes inspection.

saying these in an interview costs you the question

  • Moves repeated setup into a base class and calls it solved
  • Treats DRY as the goal for test code
  • Hard-codes expected literals that mirror shared seed data
  • Builds one large fixture because some test needs each part
  • Proposes rewriting the whole suite in one change
  • Cannot say why an expected value is what it is

context