What is triangulation in TDD, and what does the second test case force?
answer
- Named after taking two bearings
- Examples produce the rule, not confirm it
- The second case must break the specific code
- Vary one dimension, or the red is ambiguous
- Third point only when the shape stays ambiguous
basics
~20 sTriangulation is deliberately adding a second, differing example so a hardcoded or over-specific implementation can no longer pass. The two cases pin down what varies, forcing you to replace the constant with the general rule the examples share.
solid answer
~50 sTriangulation is the technique of driving generality out of an implementation with examples rather than by guessing. You pass the first test with something specific — often a literal. That implementation is correct for one point and arbitrary everywhere else. You then add a second case chosen to differ in exactly the dimension you want to generalise, and the specific implementation goes red. The only way back to green without special-casing is to write the rule both examples satisfy. Two well-chosen points are usually enough; if the shape is still ambiguous after two, add a third rather than inventing the rule. The judgement is in choosing the second case: it must vary one thing, so the pressure it applies is unambiguous. And triangulation is optional — when the general form is already obvious, write it and use the extra cases only if you actually want that coverage.
code
pseudocode · 14 linestest "attempt 1 waits 45 seconds":
assert retryDelaySeconds(attempt = 1) == 45
function retryDelaySeconds(attempt):
return 45 # green, but arbitrary everywhere else
test "attempt 2 waits 90 seconds":
assert retryDelaySeconds(attempt = 2) == 90 # red
function retryDelaySeconds(attempt):
return 45 * attempt # both green, generality driven out
test "attempt 3 waits 135 seconds":
assert retryDelaySeconds(attempt = 3) == 135 # rules out doublinggo deeper
Recall the shape: pass the first case specifically, then add a second differing case that the specific code cannot satisfy. Be able to say the second case's job is to break the implementation, not to add coverage.
Explain how you choose the second case — vary one dimension so a red bar is unambiguous — and when a third is needed because two points still fit more than one rule. Know that triangulation is optional when the general form is obvious.
Show judgement about cost: which intermediate cases earn their place in a long-lived suite, and how you decide to collapse scaffolding cases once the generalisation is done. Be ready to explain why a case that passes on arrival taught you nothing.
Own the trade-off between driving design with examples and reasoning to the implementation directly. Be able to say when a team's triangulation habit has become ritual, and what evidence — cycle time, suite growth, duplicated cases — you would use to make that argument.
## Definition Triangulation is a way of arriving at a general implementation by adding examples. You make the first test pass with something deliberately specific. Then you write a second test whose expected result differs, chosen so the specific implementation cannot satisfy both. Now the cheapest honest way back to green is the general rule. The name is borrowed from surveying: one bearing tells you a direction, two bearings intersect at a point. The key insight is *where the design pressure comes from*. In triangulation you do not reason your way to the algorithm and then write tests around it. You let the examples constrain the code until only one reasonable implementation remains. The tests are not checking your generalisation; they are producing it. ## The mechanics, step by step A subscription renewal job must decide the retry delay after a failed charge, backing off as attempts accumulate. ```pseudocode # 1. First case, passed specifically test "attempt 1 waits 45 seconds": assert retryDelaySeconds(attempt = 1) == 45 function retryDelaySeconds(attempt): return 45 # 2. Second case, chosen to vary exactly one thing: the attempt number test "attempt 2 waits 90 seconds": assert retryDelaySeconds(attempt = 2) == 90 # 3. The constant is now red. Special-casing would be dishonest: # if attempt == 1 return 45 else return 90 # The rule both examples satisfy is cheaper and is the real code: function retryDelaySeconds(attempt): return 45 * attempt # 4. Is it linear or doubling? Two points cannot say. A third case decides. test "attempt 3 waits 135 seconds": assert retryDelaySeconds(attempt = 3) == 135 ``` Step 4 is the part candidates usually miss. Two points fit infinitely many curves. You triangulate with a third example precisely when two leave the shape genuinely ambiguous — here, linear growth versus doubling. If you already know from the requirement which it is, you do not need the third case to *drive* the code, though you may still want it as documentation of the intended growth. ## Choosing the second case A second case only applies pressure if it differs in the dimension you want to generalise, and ideally in that dimension only. Vary two things at once and a red bar no longer tells you which change caused it. Useful choices, in rough order of usefulness: - **A different value of the same parameter**, as above — forces the constant into an expression. - **A boundary or degenerate value** — zero attempts, an empty batch, a period of one day. These force branches that the happy path never reveals. - **A different shape of input** — a renewal with no discount versus one with a discount — forces the second term into the formula. A weak second case is one whose expected value the current implementation already returns; it goes green immediately, applies no pressure, and adds run time without adding constraint. If a new case passes the moment you write it, either you already had the general rule or you picked the wrong case; say which. ## When *not* to triangulate Triangulation is one of the strategies for getting to green, not the only one, and it is the most expensive in cycles. If the general form is obvious — you know it is a sum, a lookup, a straightforward formula — write it directly. Kent Beck, who named the technique, describes it as what you reach for when you are genuinely unsure how to generalise; treating it as mandatory turns TDD into a ritual with a poor ratio of thinking to typing. There is also a caution about scale. Triangulating every parameter of every function produces a large number of near-identical cases. In a suite that has grown to a 340-case regression pack, dozens of cases that exist only as scaffolding for a generalisation that finished months ago are pure maintenance cost: they run on every push, they all break when a signature changes, and none of them describes a distinct behaviour. It is legitimate to collapse the intermediate points once the general implementation is in place, keeping the cases that document real behaviours — the boundary, the degenerate input, the representative middle — and deleting the ones whose only job was to apply pressure. That deletion is a judgement call, and being able to argue it is a mid-to-senior signal. ## Distinguishing it from neighbours Triangulation is often confused with two other things. It is not parameterised or data-driven testing, which runs one case over many inputs for coverage; triangulation is a sequence of separate red bars, each changing the implementation. And it is not the same as adding examples after the fact to check code you already wrote — there the code constrains the examples, which is the opposite direction of pressure.
- You add a second case and it passes immediately. What does that tell you?That the case applied no design pressure. Either the implementation was already general enough — in which case you have learned nothing and merely added run time — or you varied the wrong dimension and the current code happens to return the expected value. The useful response is to name which of the two it is: keep the case if it documents a behaviour worth pinning, delete it if it exists only as failed scaffolding.
- Do the intermediate cases from a triangulation have to stay in the suite forever?No, and keeping all of them is a real cost in a large regression pack. Once the general implementation exists, the cases whose only purpose was to apply pressure duplicate each other. Keep the ones that document distinct behaviour — a boundary, a degenerate input, a representative example — and delete the rest. It is a judgement call, and it should be a deliberate decision rather than an accident of never revisiting old cases.
- How do you avoid special-casing your way through a triangulation?By treating a chain of conditionals keyed on the exact test inputs as a signal, not a solution. If the honest general rule is not visible yet, the usual move is to write the special case, go green, and then look for the duplication between the branches — they typically differ only in a value that can be lifted into an expression. If two branches resist unification, that is often real domain distinction rather than missing generality.
Surveying gives the technique its name: one bearing narrows you to a line, a second bearing crosses it at a point, and a third confirms you did not misread either instrument.
saying these in an interview costs you the question
- Thinks triangulation just means writing more test cases
- Adds a second case that passes without changing anything
- Varies several inputs at once, so the red is ambiguous
- Treats triangulation as mandatory for every function
- Confuses it with running one case over many inputs
- Leaves a chain of special cases as the final implementation