skip to content

Adding a per-case setup step to a subclass breaks cases that passed before — how does inherited setup ordering explain it?

level: seniorimportance: should knowfreq 40%

answer

  1. No single file states the sequence
  2. Order lives in the tree, not the class
  3. The new step assumed something produced later
  4. Replacing a parent's step happens silently
  5. Arguments instead of ambient values

basics

~20 s

Preparation spread across a class chain runs in an order no single file states; it comes from the runner's rule and from whether each level invokes its parent. A new step can land ahead of what it assumed.

solid answer

~50 s

The chain has no single place where the sequence is written, so a new step can execute at a point its author never intended. Three mechanisms produce the failure: the new step reads something another step produces later; the new step replaces the parent's step rather than adding to it, so the parent's work silently stops happening; or both steps communicate through ambient values, and the child changes one the parent's later work assumed. Only the branch that changed fails, because every other case still takes the sequence it always had. The repair is to stop depending on an order you cannot read — gather preparation into one explicit sequence the case invokes, pass each step's inputs as arguments rather than leaving them ambient, and have every step assert its own preconditions so a wrong order fails with a named message instead of an absent value three assertions later.

code

pseudocode · 13 lines
pseudocode
# implicit: the effective order lives in the tree, not in a file
class Base:
    prepare(): identity = establish_identity()

class BillingCase extends Base:
    prepare(): cart = build_cart(identity)   # identity may not exist yet

# explicit: order and dependencies are written where the case is read
class BillingCase:
    run():
        identity = establish_identity()
        cart     = build_cart(identity)      # cannot run before its input exists
        assert_total(cart)

go deeper

for a junior

Know that preparation can arrive from several levels of a class chain, and that the order it runs in is not written in the case you are reading. If your new step depends on something, say so in code rather than assuming.

for a middle

Explain the three mechanisms — a step reading something produced later, a replacement quietly removing a parent's work, and steps exchanging ambient values — and why each surfaces only on the branch that changed.

for a senior

Expect a diagnosis scenario. Show that you would recover the real order from the run's own output rather than guess it, and that your fix removes the dependence on ordering instead of tuning timings until the suite is green.

for a principal

Own the position that implicit ordering is a design defect rather than a puzzle to master: preparation should be an explicit sequence with arguments, so correctness never depends on knowledge that lives only in someone's head.

## Where the order comes from When preparation is spread across a class chain, the sequence that actually executes is produced by three things, none of which is written in the class you are editing: - **The runner's own rule** for preparation declared at several levels of a chain. It has one; you cannot read it off your class, and assuming a rule you have not confirmed is how most of these defects are introduced. - **Whether each level invokes its parent's step.** Where a child's step replaces the parent's, the child must call the parent's work explicitly, and forgetting to is silent. - **Declaration position inside a single class**, when one class carries more than one preparation step. Put together, the effective order is a property of the *tree*, not of any file. That is why the failure arrives with a new subclass: everything else in the tree still has the sequence it always had, so the suite looks as if it broke for a case-specific reason. ## Three mechanisms, all of which look like flakiness 1. **The new step reads what another step produces later.** A child's step needs the signed-in identity an ancestor establishes. If the runner's rule puts the child's work first, or the child replaced the parent's step and never called it, the identity is absent. The symptom is a missing value during preparation — or worse, a fallback that quietly produces the wrong one. 2. **The new step replaces rather than adds.** Where a child's step has the same shape as the parent's, defining it can supersede the parent's entirely. Nothing fails at the point of the replacement: the case simply runs with less preparation than its siblings, and often still passes for a while, until an assertion depends on what went missing. 3. **The steps communicate through ambient values.** A parent step leaves a toggle or a default identity somewhere both steps can reach; the child's new step changes it. The parent's later work now operates on something different from what it assumed. Nothing here is an error — the two steps merely disagree, and the disagreement is invisible because the channel between them is not an argument, it is ambient. ## Why only the branch that changed fails - Every other case takes a path through the chain that the new step is not on. - The report names cases, not preparation, so "only these twelve cases" reads like a case-content problem rather than a structural one. - Re-running does not help, because the order is deterministic; teams still mislabel it as intermittent, because whether it appears also depends on which cases a given run selected. - The commit that introduced it is small and looks unrelated to the assertions that fail. ## Making the order visible The repair is not to learn the ordering rule better. It is to stop depending on knowledge that lives outside the code: | Implicit ordering | Explicit sequence | |---|---| | Order emerges from class positions | Order is written in one place | | Steps exchange ambient values | Each step takes and returns values | | A new step can silently precede its dependency | A dependency is an argument, so it cannot be missing | | Diagnosis means reconstructing the tree | Diagnosis means reading five lines | Concretely: - **Gather preparation into one ordered sequence the case invokes.** Capture, then identity, then data — visible, in that order, where the case is read. - **Pass what each step needs as an argument** and have it return what it produced, so a missing dependency becomes impossible to express rather than merely unlikely. - **Assert preconditions inside each step**, with a message naming what was expected. A wrong order should fail with a sentence, not with an absent value three assertions later. - **Emit the step names as part of the run's own output.** When something does go wrong, the real order is recoverable from the failed run instead of reconstructed from memory. - **Never repair it with a pause.** A pause guesses at timing, slows every run and converts a deterministic ordering defect into an intermittent one that returns under load. ## When you cannot change the shape today If the chain has to stay for now, the cheapest useful move is the precondition assertion: each step states what it requires and fails loudly when it is absent. That converts a class of silent, distant failures into immediate, named ones, and it costs about a line per step. The second cheapest is to stop introducing new ambient communication — a step that takes its inputs as arguments cannot fall victim to the order, even while its neighbours still can. Both are small enough to land beside the change that exposed the problem, which matters, because a defect explained but not fenced comes back with the next subclass.

  • How would you make the effective order of preparation steps visible to someone reading the case?
    Have one place that names the sequence. A case-level preparation body that invokes the steps in written order — capture, then identity, then data — is readable in a way a chain never is, because the reader is not reconstructing runner rules from class positions. Where the chain must stay, at minimum emit each step's name in the run's own output, so the real order is recoverable from a failed run.
  • A subclass replaces the parent's step and the parent's work stops happening. Why is that hard to notice?
    Because nothing fails where the replacement is written. The case simply runs with less preparation than its siblings and often still passes for a while. The signal arrives only when a later assertion depends on the missing work, by which time the replacement looks unrelated. Making each step assert its own preconditions turns a silent difference into an immediate, named failure.

It is a recipe whose steps are printed on four separate cards, with the order agreed verbally by people who have since left the team.

saying these in an interview costs you the question

  • Assumes inherited preparation always runs parent-first without checking
  • Adds a pause so the new step lands after the others
  • Reorders class declarations until the suite goes green
  • Says the runner guarantees an order without saying which one
  • Leaves steps exchanging ambient values instead of arguments
  • Labels the failing case flaky and re-runs it