Riadh Mnasri
← Back to blog
4 min read

Why TDD isn't optional when you're shipping fast

The objection I hear most often about TDD is that it slows you down when you're under pressure. In practice, it's the opposite: it's precisely under deadline pressure that writing the test before the code saves time, rather than costing it.

What pressure actually changes

Under pressure, the first thing that gets sacrificed isn't coding time, it's the time spent thinking about what you're actually building. You code faster, but you code fuzzy: the scope of the change gets clarified as you write it, instead of being clear beforehand. You discover edge cases in production, at the worst possible time, instead of while writing the test.

Writing the test before the implementation forces that clarification to happen first, when it's cheapest. A test you can't write clearly is usually a sign you haven't understood the problem yet, not a tooling problem. It's an early signal, exactly when you need one the most.

The real cost isn't where you think

The critique of TDD assumes cost is measured in time spent writing code. But on a freelance engagement, the cost that actually matters is the time spent debugging in production, and the time the next developer spends understanding the code six months later, without the context you had in mind while writing the feature. A test that documents an edge case pays back that investment very quickly, often as soon as the first bug it prevents, and then serves as living documentation for anyone who touches that code afterward.

A concrete example

For a rule that rejects a counterparty whose exposure exceeds its limit, the Given/When/Then convention produces a test that reads like a specification, before a single line of implementation exists:

describe("risk limit validation", () => {
  it("rejects an exposure that exceeds the counterparty's limit", () => {
    // Given a counterparty with a limit of 1,000,000
    const counterparty = aCounterparty({ riskLimit: 1_000_000 });

    // When validating an exposure of 1,200,000
    const result = validateExposure(counterparty, { amount: 1_200_000 });

    // Then validation fails with an explicit reason
    expect(result).toEqual({ valid: false, reason: "LIMIT_EXCEEDED" });
  });
});

This test doesn't test anything implemented: it forces you to decide, before writing a single line of logic, what "exceeding the limit" precisely means (strictly greater than? from what threshold?) and what the system should respond. That decision, made early and explicitly, is what's missing when you code the implementation first and bolt tests on afterward.

Where the cost moves, not where it disappears

Code first, tests afterTest first (TDD)
Initial writing speedFaster on the surfaceSlightly slower at the start
When the requirement gets clarifiedDuring debugging, often in productionBefore the first line of code
Cost of a forgotten edge caseA bug found by a user or an incidentA missing test that stands out in review
Documentation of expected behaviorReconstructed after the fact, if anyone bothersThe test itself, up to date by construction

That table sums up the most common objection from a different angle: TDD doesn't make the cost of thinking through edge cases disappear, it simply moves it before the code is written, when it's cheapest to fix.

What this actually changes in my workflow

On personal projects as on client engagements, the same discipline applies: a use case isn't done until its test explicitly describes its expected behavior, including error cases. I consistently use the Given/When/Then convention, which forces you to explicitly name the starting context, the triggering action, and the expected outcome, before writing a single line of implementation.

On MissionMatch or kotlin-counterparty-risk, every domain use case was written test-first. The benefit becomes especially visible on financial calculations: without a test that pins down the expected behavior for an edge case (a negative notional, a maturity date in the past, a zero collateral flow), it's easy to ship a calculation that looks correct on common cases and silently fails on rare ones.

What I tell myself under pressure

When an engagement imposes a tight deadline, the temptation is to skip tests to move faster. My experience is consistently the opposite: that's exactly when TDD protects the most, because it prevents accumulating debt you won't have time to pay back later, once the deadline has passed and attention has moved on. This isn't perfectionism: it's the only way I know to keep speed over the long run, instead of trading it for debt you pay back later, with interest.