How does writing the test first shape the interface of code that does not exist yet?
answer
- Somebody has to use it first
- Four decisions before any implementation
- Arrangement length is a cost signal
- The assertion needs a visible result
- Name, arguments, result, failure channel
basics
~20 sThe test is the interface's first caller, so it must invent the operation's name, the arguments handed in, the shape of the result and how failures are reported — all judged from the outside, before any implementation can bias them.
solid answer
~50 sWriting the test first means somebody has to use the operation before it exists, and that somebody is the test. Four decisions fall out immediately: what the thing is called, what you must have in hand to call it, what it hands back, and how it signals a case it cannot serve. Each is decided from the caller's seat, which is the seat that matters. Awkwardness is informative rather than annoying: if the call needs eleven fields of setup before it can say anything, the operation is asking the caller for too much; if you must inspect a private field to check the outcome, the result is not being reported anywhere a client could see it. Because no code exists, fixing any of this costs one edit to the test rather than a refactor across callers.
code
pseudocode · 6 linestest "a reading with no device id is rejected":
ingest = FleetIngest(clock = fixed("2027-03-04T02:00:00Z"))
outcome = ingest.accept(reading(deviceId = null, odometerKm = 41283))
assert outcome.accepted == false
assert outcome.reason == "MISSING_DEVICE_ID"go deeper
Remember that the test has to call something that is not there yet, so you choose the operation's name, what you pass it and what you get back. Being able to list those decisions is enough at this level.
Explain each decision and what its difficulty signals: long arrangement means the operation demands too much, an invisible outcome means the result has no caller-visible channel, and ambient time means a dependency was never made an argument.
Demonstrate reading test-writing pain as a design diagnosis in production code, and insist the fix goes into the interface rather than into wider visibility or a heavier fixture, because a suite that asserts on internals breaks under safe restructuring.
Own the boundary of the claim: the first test designs a call site, not a system decomposition. Be able to say where that leverage stops and which architectural decisions still need a deliberate, separate design conversation.
## The test as the first client The reason test-first is discussed as *design* and not merely as verification is that the first test is the first piece of code that uses the thing being built. Every choice a user of an interface cares about has to be made before the test compiles, and none of them can be deferred to "whatever the implementation ends up doing", because there is no implementation to defer to. ## The four decisions the first test forces **1. The name.** You must call the operation something in order to invoke it, and you choose that name while thinking about the requirement rather than about the algorithm. Names chosen from the caller's seat describe an outcome ("accept a reading", "summarise the window"); names chosen from the implementer's seat leak mechanism ("loop over the buffer and flush"). **2. The arguments.** To make the call you must assemble everything the operation needs. This is where cost becomes visible: if the arrangement is three lines, the operation asks for little; if it is thirty lines of scaffolding, it is asking the caller to know far too much, or to have built half the system first. **3. The result.** Something must come back that the assertion can look at. Deciding *what* is a genuine modelling choice — a value, a small outcome record, a stream of events — and it is being made by the party that has to consume it. **4. The failure channel.** The first test that covers an input the operation cannot serve forces the question of how refusal is expressed: a distinguishable outcome value, a raised error, or a recorded event. Test-after work tends to inherit whatever the implementer reached for first. ## A worked example Suppose a fleet telematics ingest must reject a device reading that carries no device identifier. Before any code, the test states the call: ``` test "a reading with no device id is rejected": ingest = FleetIngest(clock = fixed("2027-03-04T02:00:00Z")) outcome = ingest.accept(reading(deviceId = null, odometerKm = 41283)) assert outcome.accepted == false assert outcome.reason == "MISSING_DEVICE_ID" ``` Four decisions are already made and visible: the operation is `accept` on an ingest that takes a clock, it consumes one reading, it returns an outcome carrying a reason rather than throwing, and time is injected rather than read from the environment. That last one is typical: the test could not have been written deterministically otherwise, so the need for an injected clock surfaced as a writing difficulty rather than as an intermittent failure months later. Now contrast the same behaviour written the other way round. If `accept` had already been implemented to log a warning and return nothing, the test written afterwards would have to reach into a log capture or a private counter to see the refusal at all. The awkwardness is identical; only its timing differs, and by then several callers may already depend on the silent version. ## Reading the pain as a signal The practical skill is treating difficulty in *writing* the test as information about the design rather than as a problem with testing: - **Long arrangement** — the operation demands too much context, or is doing several jobs. - **No visible outcome** — the result is escaping through a channel a client cannot observe. - **Assertion needs internals** — the useful result was never exposed; reaching for a private field is a symptom, and a suite built that way breaks on safe restructuring. - **Non-deterministic input** — a dependency on ambient time, randomness or the environment has not been made an argument. None of these observations require the implementation to exist. That is the leverage: the cost of acting on them is one edit to a test nobody depends on yet. ## Limits worth stating The first test designs an interface, not an architecture. It settles the immediate call site and says nothing about how the work is divided behind it. It is also possible to write a first test that bakes in a bad interface — writing first guarantees only that the decision is made deliberately and early, not that it is made well. And an interface designed against a single example may be over-fitted to it; the honest position is that later examples will pull on it, and the first shape is a proposal rather than a commitment. ## What a strong answer sounds like Name the four decisions, show that they are made from the caller's seat, and give one concrete example of pain-as-signal — the injected clock or the invisible refusal are both good. Weak answers stop at "it makes the code more testable", which restates the conclusion without saying which decisions moved or why the timing matters.
- You cannot assert on the outcome without reading a private field. What does that tell you about the design?That the result the caller needs is not reported anywhere a caller can see it. The fix is to expose the outcome as a returned value or a published event, not to widen visibility for the test. A suite that asserts on internals also couples itself to structure, so a safe restructuring later turns it red for no behavioural reason.
- Does writing the test first guarantee a good interface?No. It guarantees the interface is decided deliberately, early, and from the caller's point of view, which is when changing it is cheapest. It is still possible to write a first test that fixes a poor shape. Later examples exert pressure on that shape, so treat the first call site as a proposal rather than a commitment.
- Why does the setup length of a first test carry design information?Because the setup is exactly what the operation demands of every caller. Thirty lines of arrangement means production callers must also assemble thirty lines of context, or that the operation is doing several jobs at once. Reading that as a design signal, rather than as a reason to build a bigger fixture, is the point of noticing it before the code exists.
It is like designing a door handle by first trying to open the door: you notice immediately whether your hands are full, and the fix costs nothing because the door is not hung yet.
saying these in an interview costs you the question
- Says only that test-first makes code more testable
- Widens field visibility so the assertion can see internals
- Treats long setup as a fixture problem, not a design signal
- Claims the first test settles the whole architecture
- Leaves ambient time or randomness inside the operation