
I used to think testing was a chore—a checkbox you tick after the real work, just to keep a manager calm. Then I spent a year next to an engineer who refused to write a single line of implementation before the test existed. At first, he looked slow. His pull requests took ages to surface. But his code almost never broke. His rewrites were surgical, not panicked. And his speed—measured from task assignment to a deployment that actually stuck around—left everyone else in the dust. That observation cracked something open for me. It forced me to rethink what productivity even means. This isn’t a beginner’s how-to. It’s about why the sharpest engineers I know flip the usual code-then-test sequence, and what that flip does to their thinking.
The False Economy of Writing Code First
Coding without a test feels quick. You type, refresh, see something light up. That little dopamine loop is addictive. But what you’re really doing is welding a mental model to the implementation you happen to be building right now. You learn about the problem through a solution you’re already half-married to. Engineers who start this way often end up with code that fits their first guess perfectly—and fits the real requirement poorly. They test manually, tapping the happy path a couple of times, maybe one edge case if they’re feeling thorough. Then they move on. The test suite, if it appears at all, becomes a retroactive justification: a set of assertions that describe what the code does, not what it should do.
That’s how brittle systems grow. Tests written after the fact are glued to the current implementation. Change the implementation and they crack in ways that don’t flag a defect—they just scream “something shifted.” That noise teaches a team to ignore failing tests. I’ve watched whole suites get commented out because they were too annoying to keep alive. The root cause wasn’t sloppiness. It was the sequence. The test was never an independent spec. It was a mirror held up to code that already existed.
Now picture an engineer who writes the test first. She has to name the thing she’s building before she builds it. She has to decide what success looks like from the outside. She’s cornered into confronting the interface—inputs, outputs, error states—before she’s invested in any particular internal structure. That constraint feels like a straightjacket for about ten minutes. Then it turns into a lens. The problem snaps into focus.
The Test as a Specification, Not a Verification

Most of us treat tests as verification gear. They prove the code does what it’s supposed to. True, but shallow. The deeper thing: a test written first is a specification—a formal, runnable description of expected behavior. When you write expect(cart.total).toBe(0) before Cart even exists, you’ve planted a claim in the world. It’s public. Unambiguous. A machine can check it. It outlasts your memory of the code. It tells whoever reads it six months later—including you—what you intended.
I once watched a senior engineer spend forty minutes on a single test for a payment-calculation edge case. He wasn’t stuck. He was wrestling with himself over the right behavior when a discount code expired mid-checkout. The test captured that fight. By the time he wrote the implementation, the logic was almost boring—three lines that satisfied a painstakingly negotiated contract. If he’d started with code, he’d have banged out a solution in five minutes, and it would have been wrong in a way that only surfaced during a billing audit. The test-first discipline didn’t slow him down. It moved the heavy thinking to where it’s cheapest: before a line of production code exists.
This shift—from verification to specification—rewires how you design. You stop asking “How do I make this work?” and start asking “What does ‘works’ mean, exactly?” The first question births code that’s incidentally correct. The second births code that’s provably correct, inside the boundaries of the spec you wrote. And if the spec is wrong, you’ll discover it when the test fails against reality, not when a customer opens a ticket.
Design Feedback in Milliseconds, Not Hours
There’s another, less obvious payoff: test-first squashes the feedback loop on design quality. A test is your code’s first client. If the test is awkward to write—too much setup, too many mocks, too much peeking at internal state—the design is whispering something. The interface stinks. The coupling is too tight. The abstraction is leaking.
Engineers who code first don’t hear that whisper. They’re not trying to use their own interface from the outside. They’re swimming inside the implementation, where everything feels natural because they built it. But the test author is a user. When she struggles to instantiate an object because the constructor demands seven dependencies—three of them irrelevant to the behavior under test—she’s spotted a design flaw. When the test can’t assert anything without wiring up database state, network state, and file system state, she’s found another.
I’ve seen this replay too many times to count. An engineer writes a test, winces at the setup, and refactors the interface before writing a stitch of implementation. The result is code that’s testable by construction, which almost always means it’s modular, composable, and easier to reason about. The alternative—coding first and then trying to bolt on testability—is like framing a house and then remembering you need plumbing. You can do it, but you’ll wish you’d thought sooner.

The Cognitive Discipline of Asserting Before Acting
There’s a psychological layer here that barely gets talked about. Writing a test first forces you to hold two clashing mental states at once: you have to be specific enough to pin down expected behavior, and humble enough to admit you might be wrong. The test says “this is what should happen.” The red bar says “but it doesn’t yet.” Sitting in that tension—between intention and reality—builds a strain of intellectual honesty that’s tough to cultivate any other way.
Engineers who only test after coding never sit in that tension. They write code, it passes their manual poking, and they feel finished. Then they write tests that pass instantly, because they’re testing the code they just wrote. The green bar becomes a ceremony, not a discovery. That’s not testing; that’s celebration. And celebrations don’t catch bugs.
The best engineers I know get twitchy around code that hasn’t been challenged. They want the test to fail first. They want to see the red bar, because a test that passes before you write the code is either useless or you’re testing the wrong thing. They treat the red-to-green transition as proof they’ve built something new, not just dressed up something old. That unease with unearned green is a marker of engineering maturity. It’s the gap between someone who wants to be done and someone who wants to be correct.
What This Looks Like in Practice
I’m not painting a utopia where every line of code flowers from a perfect test. Real systems have scruffy boundaries. UIs resist isolation. Third-party integrations demand fakes that sometimes drift from reality. The test-first approach doesn’t erase these headaches. It makes them visible and wrangleable. When a test is too hard to write, that’s data. It tells you where the complexity lives and invites you to quarantine it behind a clean interface.
The engineers I admire don’t cling to rigid dogma. They don’t test-first every single function. But they default to it. They write the test when the behavior matters enough to specify—which is most of the time. They skip only when the cost of testing outweighs the cost of a defect, and they’re blunt about that tradeoff. And when they do skip, a faint unease settles in—a recognition that they’re working without a net.
If you want to try this, start tiny. Pick one feature, one bug fix, one refactor. Write the test first. Watch how your questions shift. Notice when the test setup aches, and use that ache to improve the design. Pay attention to the moment the test passes, and ask whether the passing code is something you’re proud of, or just something that works. The shift feels foreign for weeks and automatic only after months. But once it clicks, you’ll read your own old code differently. You’ll spot the places where you guessed instead of reasoned, where you verified instead of specified.
Frequently Asked Questions
Isn’t writing tests first slower?
It’s slower in the first hour. It’s faster across the life of the feature. The time you sink into the test upfront is time you don’t spend later debugging, reworking lousy interfaces, or manually checking behavior that should be automated. The engineers I’ve watched who practice this consistently ship more features per quarter, not fewer. The trick is measuring from commit to stable deployment, not from first keystroke to first green bar.
What if I don’t know the design well enough to write a test?
That’s the exact moment to write a test. The test forces you to turn the unknown into something known. It shoves you to define the interface, the inputs, the outputs before you’re snarled in implementation. If you still can’t write the test, you probably need to spike—hack together throwaway code to explore the problem, then delete it and start with a test. The spike feeds the test; the test feeds the design.
Does this work for all types of code, like UI or infrastructure?
It works wherever you can define expected behavior from the outside. For UIs, that might mean component tests that render and poke the component. For infrastructure, it might mean tests that verify configuration spits out the expected resources. The principle stays the same: specify before you build. The techniques shift, but the mindset doesn’t.
How do I convince my team to try this?
Don’t argue. Show them. Grab a non-critical bug fix, write the test first, and demonstrate how the test nailed the exact failure mode before you fixed it. Then do it again on a small feature. Let the results do the talking. Engineers trust evidence, not rhetoric. When your code breaks less and your design reviews glide, people will start asking what you’re doing differently.