
For the first two years, our team treated software testing as a checkpoint. Code got written. Code got reviewed. Code got tested. Code got shipped. The testing step sat between review and release, and its job was to confirm that what we built matched what we intended to build. We had a reasonable suite of automated tests, a QA engineer who owned the process, and a clear definition of done that included a green pipeline. By most measures, we were doing things correctly.
What we did not have was any real confidence in our deployments. Not the visible, dramatic kind of lacking confidence where everyone is nervous and the process feels chaotic. The quieter kind, where you merge and immediately start half-watching the monitoring dashboard. Where Friday afternoons carry a specific quality of low-level unease. Where someone always volunteers to be available over the weekend after a significant release, just in case. The tests were passing. The pipeline was green. And we were still, collectively, a little bit afraid of our own software.
It took an incident, and then a long conversation after that incident, to understand why.
The incident was not dramatic. A downstream service changed how it handled a particular class of request. Our service, which depended on that behavior, started failing silently for a subset of users. Not crashing, just returning subtly wrong results in a way that took eleven days to surface through customer support tickets.
When we traced it back, the cause was embarrassingly simple. Our integration tests for that dependency were running against a mock we had written eight months earlier. The mock reflected how the downstream service behaved in April. It was now December. The downstream service had deployed four times in the intervening months, and our mock had not moved with it.
The tests passed because they were checking the right things against the wrong version of reality. That conversation afterward was where the mindset shift started. We had been operating from a testing definition that went something like this: testing confirms that code does what it was written to do. This definition is not wrong. What we realized, sitting around that post-mortem, is that it is incomplete in a way that had been quietly producing false confidence for years.
The more complete version of the definition is this: testing confirms that code does what users need it to do, continuously, as the system around it keeps changing. The addition of continuously and as the system around it keeps changing is not a small amendment. It changes almost everything about what good software testing practices actually look like.
Before unpacking what changed, it is worth being honest about why the old model felt like enough for so long.
Understanding software testing basics- unit tests covering individual functions, integration tests covering component interactions, end-to-end tests covering user journeys, gives a team a genuinely solid foundation. These layers catch a real and significant proportion of the failures that would otherwise reach production.
For a system with a handful of services and a team small enough that everyone knows what is happening across the codebase, this foundation is often sufficient.
It stops being sufficient at a specific and predictable point: when the system has grown complex enough that no individual holds a complete mental model of all of it, and when services are changing often enough that the assumptions encoded in tests start drifting from the reality they are supposed to represent.
We hit that point somewhere between our eighth and twelfth service. We did not notice when we crossed it, because the metrics that would have told us- test pass rates, coverage percentages, deployment frequency: all kept looking healthy.
The degradation was in something the metrics were not measuring: the gap between what our tests were checking and what our system was actually doing.
The shift was not from less testing to more testing. It was from testing as verification to software testing as ongoing knowledge maintenance.
Verification is a point-in-time activity. You verify that a thing is true at a specific moment. It answers the question: is this correct right now? For a system that does not change, verification is sufficient. For a system where multiple services deploy independently on their own schedules, verification answers a question that stops being relevant the moment any dependency changes.
Knowledge maintenance is a continuous activity. It answers a different question: do we still know what is true? Applied to software testing, it means treating the test suite not as a record of what was correct when tests were written, but as a living representation of what is correct now, which requires actively maintaining the currency of what the tests know, not just the accuracy of what they check.
This reframe changed our testing capabilities in three specific ways:
First, we started treating mocks as the most important maintenance artifact in our test suite, not as boilerplate. A mock that is outdated is not a minor inconvenience. It is a gap between what we believe is true about our system and what is actually true. That gap produces exactly the kind of false confidence that let our December incident stay hidden for eleven days.
Second, we started asking a different pre-deployment question. Instead of "did the tests pass," we started asking "are the tests that passed still checking the right things against current reality." These sound similar. They lead to very different behavior. The first question has a binary answer visible in the pipeline. The second requires knowing when mocks and integration assumptions were last validated against their dependencies' actual current behavior.
Third, we started treating a test failure differently. Previously, the instinct when something failed unexpectedly was to investigate whether the test was wrong- whether it was checking something that had legitimately changed. That instinct is not incorrect, but it was our first instinct when it should have been our second. Our first instinct now is to ask whether the failure is telling us something true about the system that we did not know.
Most unexpected failures are. The ones that are not= the genuinely stale or misconfigured tests, reveal themselves quickly when investigated properly.
The team we are now approaches testing with a different set of baseline assumptions than the team we were before that incident.
We assume our integration assumptions are decaying constantly, because they are. Every downstream deployment is a potential drift event. We do not treat this as a crisis to be managed through more rigorous mock maintenance processes- we have tried that, and it does not scale past a certain service count. We treat it as a structural property of the system that requires structural solutions rather than process ones.
Passing tests are only as valuable as the accuracy of what they are checking against. A green pipeline built on outdated mocks is not a green pipeline. It is a false signal that costs more than no signal because it actively creates confidence where none is warranted.
Coverage is not a static property of a test suite but a continuously degrading one that requires active investment to maintain. The investment is not always in writing new tests. Sometimes it is in validating that existing tests are still checking what we think they are checking against dependencies that have since changed.
None of these assumptions are comfortable. They are more demanding than the old model, which let us declare victory on testing once we hit a coverage target and kept the pipeline green. But they are accurate in a way the old model was not, and accuracy is what actually produces the confidence that makes deploying on a Friday afternoon feel like a normal engineering activity rather than a minor act of faith.
The mindset shift did not make our software better overnight. What it did was make us more honest about what our testing was actually telling us, and more deliberate about making sure it was telling us something true.