A TDD increment now needs hundreds of lines to go green. How do you split it into smaller steps?
answer
- Debugging inside the red bar is the tell
- Name the behaviour's independent dimensions
- Start from the most degenerate input
- Undriven behaviour may stay absent
- Discard the attempt, keep the understanding
basics
~20 sSplit along the behaviour's degrees of freedom: pick one input dimension or one outcome, write a case that varies only that, and let the rest stay unimplemented or degenerate. Each step should be a single edit you can reason about between two green runs.
solid answer
~50 sThe tell that an increment is too large is that you stop driving and start debugging: the case fails for several reasons at once, you cannot name it after one behaviour, and you are stepping through code to find out why the bar is red. The fix is decomposition, not perseverance. Enumerate the degrees of freedom in the behaviour — how many items, whether each succeeds, what happens on failure, what is reported — and drive them one at a time, starting from the most degenerate input the behaviour admits. Everything you have not driven yet may legitimately be absent or hardcoded; a later case removes each stand-in value. When you cannot see a decomposition, the honest move is to abandon the current attempt from the last known-good state and re-plan the sequence, rather than growing the step until it works.
code
pseudocode · 10 lines# BEFORE: one case, hundreds of lines to go green
test "charges every due subscription and rolls the whole batch back on any failure"
# AFTER: one dimension per step
test "an empty batch charges nothing and reports no failures"
test "a batch of one due subscription charges 5300 cents once"
test "a batch of one whose charge is declined reports that subscription"
test "a batch of two due subscriptions charges both"
test "when the second of two charges is declined, the first is reversed"
test "a declined charge in a batch of three reverses both earlier charges"go deeper
Recall the basic signal: if a single failing case needs a lot of code to pass, write a simpler case first. Starting from an empty or single-item input is almost always available and almost always smaller.
Explain the decomposition itself — list the behaviour's independent dimensions and drive one per step — and why splitting a multi-outcome assertion into separate cases makes each red bar name a single suspect.
Demonstrate you have lived it: describe the moment you noticed you were debugging inside the red bar, the sequence of cases you chose instead, and the decision to discard a half-finished attempt and re-plan from the last green state.
Own the economics without overclaiming. Argue feedback locality — a small edit means a small suspect set, and narrow cases keep a large regression pack diagnosable — and be explicit that the popular defect-cost multipliers are contested rather than settled.
## Recognising the problem A test-driven increment is supposed to be a short round trip: one failing case, a small edit, green. When the edit needed to reach green becomes large, several things go wrong at once. The red bar stops being informative, because the case can fail for many independent reasons and the failure message names only the first. You lose the ability to attribute a change to an outcome, so the moment something breaks you reach for a debugger — at which point the test is no longer driving the design, it is a slow way of running the program. And the window during which you cannot safely restructure anything stretches from seconds to the length of the whole implementation. The named symptoms worth being able to state in an interview: the case's name contains "and"; the arrange section is longer than the code you are about to write; you cannot predict which assertion will fail first; you have been red for long enough to have forgotten your last edit. ## The decomposition method Treat the behaviour as a set of independent dimensions and drive one at a time. Take a subscription renewal job whose next increment is: *charge every due subscription in a batch, and if any charge fails, roll the whole batch back and report which one failed.* That is at least five dimensions — batch size, per-item success, the rollback, the report, and the ordering guarantee — and driving them together is the hundreds-of-lines increment. The sequence that keeps each step small: ```pseudocode # 1. Most degenerate input the behaviour admits test "an empty batch charges nothing and reports no failures" # 2. One item, happy path -- introduces the charge call, nothing else test "a batch of one due subscription charges 5300 cents once" # 3. One item, sad path -- introduces the failure outcome only test "a batch of one whose charge is declined reports that subscription" # 4. Two items -- introduces iteration, still no rollback test "a batch of two due subscriptions charges both" # 5. Two items, second fails -- now, and only now, rollback exists test "when the second of two charges is declined, the first is reversed" # 6. Ordering, reporting detail, partial-failure edge cases test "a declined charge in a batch of three reverses both earlier charges" ``` Three properties make this work. **Each step adds one reason for the code to change.** Step 4 introduces iteration and nothing else; if it goes red, iteration is the suspect. **Everything not yet driven may be degenerate.** After step 2 the code can charge exactly one item and know nothing about rollback; that is not an unfinished feature, it is a correct implementation of the cases that exist. **The steps compose forwards.** Step 5 does not invalidate step 3; it constrains a dimension step 3 left free. ## Other ways to shrink a step When a dimension itself is too large, further tools: - **Split the assertion.** A case that asserts on the charge, the reversal and the report at once is three cases wearing a trench coat. Assert one outcome per case while driving; the combined check, if you want it, is a later addition. - **Push the hard part behind a boundary.** Drive the orchestration against a collaborator role that does not exist yet, standing it in for now, and drive the real one separately. This changes the step from "implement the whole thing" to "implement the coordination". - **Take the degenerate case first.** Empty, zero, one, absent. These force the skeleton — the signature, the return shape, the error channel — with almost no logic, and everything afterwards is an extension rather than a start. - **Write the assertion you wish you could make, then reduce it.** If it is unwritable, the ambiguity is in the requirement, and no step size will rescue it. ## Knowing when to abandon the attempt Sometimes the decomposition is not visible from inside the red bar. The standard advice is unglamorous: return to the last state where everything was green, throw away the half-finished edit, and plan the sequence of cases before typing again. Practitioners find this hard because the discarded work feels expensive. It usually is not — the code you throw away is the code you did not understand, and you keep the understanding. What is genuinely expensive is a long red period ending in a debugging session. ## The cost argument Small steps look slower per step and are usually faster per feature, because the expensive part of development is not typing, it is locating the cause of a surprise. With a two-minute increment the search space for any new failure is one small edit. There is a second, longer-term effect: cases driven one dimension at a time tend to be narrow and independently meaningful, which is what keeps a large regression pack — say a 340-case suite — diagnosable, because one broken behaviour turns a handful of cases red rather than forty. Be careful how you claim this in an interview: the general defect-cost curve is often quoted with confident multipliers, and those specific numbers are contested. The defensible statement is about feedback locality — smaller steps mean a smaller suspect set — not about a numeric payback ratio.
- After step two the code handles exactly one item and knows nothing about rollback. Is that not an unfinished implementation?It is a complete implementation of the cases that exist, which is the point. Undriven behaviour is allowed to be absent or degenerate, because the next case is what introduces it. The risk is stopping there and shipping, so the increment sequence has to be planned and finished, not abandoned mid-way. If someone can genuinely ship after step two, the missing cases are missing requirements and that is worth surfacing.
- How do you decide the order of the split cases?Most degenerate first, because it forces the signature, the return shape and the error channel with almost no logic. Then the simplest happy path, then the first failure outcome, then multiplicity, then interactions between them. The heuristic is to add one reason for the code to change per step and to defer anything that requires a structure you have not needed yet, so structure arrives when a case demands it rather than when you predict it.
- You cannot see any decomposition and you have been red for a long time. What now?Return to the last fully green state, discard the half-finished edit, and plan the case sequence away from the keyboard. The instinct to push on is the expensive option, because a long red period usually ends in debugging rather than in design. The discarded code is not wasted: what you learned about why the behaviour resisted splitting is exactly what makes the second attempt's sequence obvious.
saying these in an interview costs you the question
- Pushes on and debugs instead of shrinking the step
- Writes one case asserting several outcomes at once
- Thinks undriven behaviour must be implemented anyway
- Refuses to discard a half-finished edit on principle
- Starts from the most complex case rather than the degenerate one
- Quotes a precise defect-cost multiplier as settled fact