An object mother has grown into a god fixture every test depends on - how do you unpick it?
answer
- One helper everything reaches for
- Changing a default reddens the suite
- Split by domain area first
- Names on top, defaults underneath
- Migrate a slice per release cycle
basics
~20 sMeasure what depends on each method, then convert the mother into thin named methods over per-area builders so tests state their own preconditions. Migrate incrementally: new tests use builders, old ones move as they are touched.
solid answer
~50 sFirst diagnose. A god fixture shows itself when one helper owns dozens of named cases across unrelated areas, when methods build far larger object graphs than any single test needs, when names describe tests rather than domain states, and above all when changing one default reddens hundreds of unrelated tests. That last symptom is the real cost: the helper has become a dependency nobody can change safely. The move is to split it by domain area - each area gets a builder holding defaults, and the surviving named methods become thin front doors onto those builders. Tests that depended on incidental defaults get those values stated locally, which is the work and also the point. Do it incrementally: forbid new methods on the old helper, route new tests through builders, and migrate a slice per release cycle rather than in one commit that no reviewer can read.
code
pseudocode · 11 lines// step 1: old name kept, now delegating - nothing breaks
function heldAtCustoms():
return customsBuilders.consignment().customsStatus(HELD)
// step 2: callers with one-off needs inline the chain and drop the name
test "reroute keeps declared value":
parcel = customsBuilders.consignment()
.customsStatus(HELD)
.declaredValueMinor(48350)
.build()
assert gateway.reroute(parcel).declaredValueMinor == 48350go deeper
Recall the warning signs you can see from one test: a setup call that builds far more than the case needs, and a helper method named after a test rather than a domain state.
Be ready to describe the target shape - a builder per area holding defaults, thin named methods on top - and why an old method should delegate to the new builder instead of copying its values.
Show the migration plan and its sequencing: measure callers, freeze the helper, delegate, then push accidental defaults into the tests that actually depend on them.
Own the trade: argue for the work on maintenance grounds, pace it against delivery, and set the ownership and review rules that stop the shared helper re-forming.
## Recognising the god fixture Shared construction helpers grow the way shared utility classes grow: each addition is individually reasonable. A parcel-tracking gateway team's consignment mother started with four methods. Three years later it exposes 41, covering consignments, carriers, customs declarations, billing accounts and route plans, and 214 of the suite's 268 tests call at least one of them. The diagnostic symptoms: - **Unrelated concerns in one helper.** Carrier setup and billing setup share a file only because both were needed by some test once. - **Over-built graphs.** A method returns a consignment with a full route plan and six tracking events because one caller needed them; every other caller pays for that construction and reads past it. - **Test-shaped names.** `consignmentForRerouteTest` names a caller, not a domain state, so it cannot be reused and its meaning dies with that test. - **No safe change.** Adjusting one default - a weight band, a default carrier - fails a large, arbitrary set of tests, because cases silently depend on values they never mention. This is the symptom that turns an inconvenience into a blocker: the helper is now change-hostile, and teams start copying methods rather than editing them, which accelerates the growth. - **Behaviour inside the fixture.** The worst ones have grown assertions, persistence writes or conditional branches on which caller is asking. ## The target shape The endpoint is not "no shared helpers" - that trades one problem for duplicated construction everywhere. It is a two-layer arrangement: 1. **A builder per domain area**, owning defaults for that area's records and nothing else. Defaults are valid, unremarkable, and computed at build time. Cross-area references are parameters, not implicit lookups. 2. **A thin naming layer** - the surviving mother methods - each one a few lines that load a builder to a state the domain actually has a name for, and return the builder rather than a finished object so a caller can adjust one field. Two rules make the split stick. A named method must correspond to a state the business would recognise, and no method may assert, persist or branch on its caller. ## Getting there without a big-bang rewrite - **Measure first.** Count callers per method. The distribution is always long-tailed: a handful of methods carry most of the suite, and a long tail has one or two callers each. The tail can be inlined into its callers as an explicit builder chain almost mechanically, and it is usually a third to a half of the methods. - **Freeze the old helper.** New methods are not accepted on it. That alone stops the growth while the rest of the work is scheduled. - **Split by area, not by test.** Move the carrier methods together, then customs. Each move is a mechanical delegation at first: the old method keeps its name and forwards to the new builder, so nothing breaks and reviewers can read the diff. - **Then push the defaults out.** Where a test's assertion turns on a value the old default happened to supply, state it in the test. This is the part that takes judgement, because you have to decide which dependencies were meaningful and which were accidental - and it is exactly the work that makes the next default change safe. - **Pace it.** On a team shipping on a three-week release train, one area per train alongside feature work is a realistic pace; the freeze plus a boy-scout rule ("if you touch a test, move it") does much of the rest without a dedicated project. ## What you can promise, and what you cannot Be honest in an interview about the payoff. The refactor does not make tests faster or find defects; it buys the ability to change a default without a day of triage, and it makes each test readable in isolation. Those are maintenance benefits, and they are worth asking for on those terms rather than dressed as quality improvements. There is a real risk to name too: while migrating, a state can exist in two places - the old method and the new builder - and drift apart, so one set of tests exercises a consignment shape production stopped producing. Keep the old method delegating to the new builder rather than duplicating its defaults, and the drift cannot happen. ## The related trap Once construction is centralised, the tempting next step is to build the **expected** value with the same helper the input came from. Then the test compares one product of the fixture with another, passes even when the behaviour is wrong, and hides the bug it was written for. Expected values belong in the test, written out literally.
- How do you tell an accidental dependency on a default from a meaningful one while migrating a test?Change the default and see whether the test's assertion still makes sense. If the case fails but its intent is unrelated to that field, the dependency was accidental and the value should be stated locally or the assertion loosened. If the case is genuinely about that value, it was meaningful and belongs in the test explicitly - which is the same outcome, reached deliberately.
- Why is building the expected value with the same helper that built the input a problem?The assertion then compares two outputs of the same construction code, so an error in that code cancels out and the test passes while the behaviour is wrong. It also passes when the code under test does nothing at all. Expected values should be written literally in the test, so the check has an independent source of truth.
- How do you stop the split helpers from growing back into the same shape?Give each area's builder an owner, cap what a named method may do - no assertions, no persistence, no branching on the caller - and require that every name maps to a domain state rather than a test. Review new named methods the way you review production API additions, and let one-off variations stay inline in the test that needs them.
saying these in an interview costs you the question
- Proposes rewriting every test in one large commit
- Keeps adding named methods to stop tests breaking
- Duplicates the old defaults into the new builder
- Claims the refactor will make the suite faster
- Leaves assertions and persistence inside the helper
- Builds the expected value with the same helper as the input