There’s a quiet little ritual in a lot of engineering teams. A feature branch sits open for days, sometimes weeks. It passes review with a handful of scattered notes about naming and structure. It gets merged. The CI pipeline glows green. Then, almost like an afterthought, a fresh ticket appears in the backlog: “Add tests for the new payment flow.” Nobody mentions it during standup. The ticket starts to gray. Eventually someone grabs it, stares at code they never wrote, and figures out the testability was never there. They aren’t writing tests. They’re digging through old assumptions with a spoon.
Nora Ishikawa here. I’ve watched this exact loop play out in startups and bloated enterprise orgs. The rationalizations sound tidy: “We had to ship fast.” “The interface was still settling.” “We’ll cover it next sprint.” Translated honestly, those phrases mean one thing: we decided to delay understanding. The feature works on the happy path, under the exact conditions of the developer’s machine. Everything else is just crossed fingers.
Why “Test Later” Is a Contradiction
Tests aren’t a separate line item. They’re a spec of behavior that happens to run. When you write tests alongside production code—or, better, before it—you’re forced to tackle questions that design docs and whiteboard chats leave fuzzy. What should happen when the upstream service times out? How does the system react to a malformed payload? Can this function even be called by itself, or does it demand half the app to be bootstrapped first?
Post-merge testing flips the whole thing upside down. The code is already a fait accompli. The engineer writing the tests now has to reverse-engineer intent. They squint at a conditional branch and wonder: was this defensive, or is that state actually reachable? They can’t ask the original author, who’s either on leave, buried in another project, or just gone. So they do the safe thing: they test the visible outputs, mock heavily, and tiptoe around the internal edge cases. The resulting test suite is a monument to surface-level verification. It passes. It also lies to you about coverage.

The Architecture Rot You Can’t See
The deeper damage isn’t even in the test suite. It’s in the production code. Writing tests early exposes design flaws right away. A function that fights you during testing usually fights you in usage, too. It probably swallows too many parameters, hides side effects, or leans on global state. When you write tests first, you feel that friction and you fix the interface. When you write them after the merge, the interface is frozen. You can’t refactor without endangering a feature that’s already in production and maybe already used by paying customers.
So you adapt to the bad design. You write test helpers that painfully set up internal state. You mock at levels that turn the test into porcelain. You drop comments like “this is necessary because of how OrderProcessor initializes.” Each of those comments is a tiny flag marking a spot where the code resisted understanding. Over time the test file becomes a museum of workarounds. New team members read it and learn the wrong lessons: that this complexity is normal, that mocking the database for a unit test is standard, that you don’t question the shape of the code—you just wrap it in enough scaffolding to make the assertions pass.
The Social Cost of Deferred Testing
There’s a people problem here, too, and it tends to get ignored because it doesn’t show up on a dashboard. When tests get written after the fact, they’re handed disproportionately to junior engineers or to folks who weren’t in the room for the original build. The signal is quiet but unmistakable: testing is lower-status work. The “real” engineering was the feature. The cleanup belongs to somebody else.
This creates a two-tier system. The people designing and building move fast, leaving a trail of untested code behind them. The people testing inherit that code without context, without the authority to refactor, and without much recognition. They burn out. They start hunting for teams where testing is part of the definition of done, not a separate chore. The team’s velocity looks fine on a chart—features are shipping—but the real stability is a shared illusion.

Coverage Numbers Are Not a Shield
Teams that live in the post-merge testing world often lean hard on code coverage metrics. “We’re at 85%,” they say, as if that figure settles every argument. But coverage tells you which lines got executed during a run, not which behaviors were actually verified. A test that calls a function and ignores the return value counts. A test that asserts true == true after running a thick pipeline counts. When tests get written after the merge, the incentive is to hit lines fast, not to interrogate behavior.
I’ve seen coverage reports that look spotless while entire error-handling paths sit completely unchecked. The test for a network timeout exists, but it mocks the HTTP client so heavily that the real retry logic never exercises. The test for an invalid input exists, but it passes because the function throws any exception, and the test only checks that an exception is thrown—not which one, not what message it carries. These are the seams that break in production at the worst possible hours.
What Test-First Actually Enforces
The discipline of writing tests before the implementation—or at least right alongside it—isn’t about dogma. It’s about forcing a specific order of thinking. You have to decide what success looks like before you build the thing that’s supposed to succeed. That decision process surfaces ambiguity. If you can’t write a test because you don’t know what the output should be, you don’t understand the requirement yet. That’s useful information. It’s cheaper to learn that before you’ve written three classes and a database migration.
Test-first also creates a natural check on complexity. If setting up the test demands five mocks, a fake event bus, and a temporary directory, the code is telling you something. Listen. The post-merge approach silences that feedback. The code is already written; the window for design changes has closed. You just pay the complexity tax and keep moving.

The Economic Argument That Fails
The most popular defense of post-merge testing is economic. “We don’t have time to write tests during the sprint.” This framing treats testing as an extra cost stacked on top of feature work. The math looks simple: feature work takes X hours, testing takes Y hours, so doing both takes X+Y hours. If we defer testing, we can ship the feature in X hours and pay Y later.
This only holds if Y stays constant no matter when you do it. It doesn’t. Writing tests for code you just wrote, while the design is still fresh, is faster than writing tests for code you haven’t touched in three weeks. Writing tests for code that was built to be testable is dramatically faster than writing tests for code that wasn’t. The Y in “test later” often runs two or three times the Y in “test now.” The economic argument pretends the friction it creates doesn’t exist.
And that’s before you count the bugs that slip past. A feature without tests is a feature whose behavior is undefined except by watching it in production. When it breaks—and it will—the debugging time, the customer hit, and the emergency patch cycle all carry costs. Those costs don’t show up on the original sprint burndown chart. They show up later, in incident reviews and lost sleep.
How to Stop the Cycle
Breaking this pattern takes a shift in how the team defines “done.” The sharpest lever I’ve seen is a simple rule: no merge without tests that the reviewer can run and understand. This isn’t about hitting a coverage number. It’s about making the tests part of the review conversation. When a pull request lands, the reviewer should be able to read the tests and grasp the intended behavior. If the tests are missing, the review isn’t finished. If the tests are confusing, the code is probably confusing, too.
This flips the social dynamic. Testing stops being a cleanup task and becomes a communication tool. The person writing the code knows their tests will be read by a colleague. They have a reason to write tests that are clear, that probe the edge cases they’re aware of, and that don’t demand an elaborate setup ritual. The person reviewing the code uses the tests as a spec. Disagreements about behavior surface during review, not during a post-merge testing ticket that nobody really wants to grab.
For legacy codebases where the pattern is already baked in, you can’t flip a switch. What you can do is draw a line: all new code, and all modified code, requires tests. Not “should have” tests. Requires. That means when someone fixes a bug, they write a test that reproduces the bug first. When someone adds a new endpoint, the test exists before the merge. The untested legacy core stays frozen until it’s touched, but it stops growing. Over time, the parts of the system that change most often become the parts with the strongest test coverage. That’s exactly what you want.
FAQ
Isn’t it sometimes necessary to merge without tests to hit a critical deadline?
Deadlines are real, but the question assumes merging untested code is actually faster. The code still has to work. Without tests, you’re verifying behavior by hand, which is slow and incomplete. If the deadline is so tight that you can’t write tests, you’re probably also trimming corners on design and review. The right answer to a brutal deadline is to cut scope, not to skip the verification step. Ship a smaller, solidly tested feature. The untested parts that “saved time” will steal more time in the following weeks than you ever saved.
What about exploratory code or prototypes? Do those need tests too?
Prototypes that never touch production don’t need tests. The headache is that prototypes have a way of turning into production code without anyone formally deciding that’s what happened. If the code is going to get merged into the main branch and shipped, it’s not a prototype anymore. Throwaway code should stay on a branch. The moment it’s merged, it’s real code, and real code needs tests. If you need to explore an idea in production, do it behind a feature flag and still write the tests. The tests guard you when the flag comes down and the behavior becomes permanent.
Our team has a dedicated QA engineer. Doesn’t that make post-merge testing acceptable?
QA and automated testing fill different buckets. QA explores the system from the outside, often catching things automated tests miss. But QA can’t check every code path, every edge case, every release. Automated tests are the developer’s statement: “I’ve thought about how this can fail, and here’s what should happen.” Leaning on QA instead of tests means pushing the thinking downstream. The developer who wrote the code is in the best spot to know its weak points. If they don’t encode that knowledge in tests, it’s gone. QA will find some of the problems, but not all of them, and usually later in the cycle when fixes cost a lot more.