A Testing Strategy That Actually Ships: Unit, Integration, and E2E
A Testing Strategy That Actually Ships: Unit, Integration, and E2E
If your test suite drinks more CI minutes than a team of developers drinks coffee, you don’t have a safety net—you have a liability. I’ve watched teams pour months into achieving 95% line coverage, only to ship regressions that a handful of well-placed integration tests would have caught. A good software testing strategy doesn’t cover every line of code; it covers the failures that actually hurt.
Most teams don’t need more tests. They need the right tests in the right proportion. The goal is to surface real breakage fast, without chaining your delivery cadence to a slow, brittle suite that nobody trusts.
The Pyramid Isn’t a Blueprint
The testing pyramid is a useful mental model, but it’s become a crutch. Treating it as a rigid recipe—lots of unit tests, a few integration tests, even fewer end-to-end tests—ignores the shape of your system. In a system with heavy external dependencies, a database with complex stored procedures, or a message bus driving critical workflows, the useful split looks very different.
A better framing: let risk and observability dictate the test investment. If a failure in a given component causes a Sev1 incident, that component demands a test that actually exercises the failure path. That’s rarely a unit test.
Unit Tests: The First Line of Defence
Unit tests are the sharpest tool for verifying pure logic. They’re fast, deterministic, and cheap to maintain. When you’re working on a calculation engine, a parsing routine, or a state machine, a well-factored unit test gives you feedback in milliseconds. That speed makes refactoring safe and keeps the development loop tight.
When They Shine
- Core domain logic that doesn’t touch I/O.
- Algorithms with edge cases that are easy to enumerate.
- Code that multiple services will consume, where a contract must be pinned precisely.
When They Waste Time
- Mocking a database adapter to test that a repository method calls
SaveChanges. You’re not proving the system works; you’re proving the mock works. - Testing getters and setters. The compiler already does that.
- Testing thin orchestration layers that only delegate to other components. The cost of maintaining those tests usually exceeds the value.
A unit test that requires a thousand lines of mocks to run is a sign that the code under test likely needs a different kind of test—or a different design.
Integration Tests: Where the Rubber Meets the Road
Integration tests verify that the parts you control actually work together. They’re the single most undervalued category in most test suites I see. A well-scoped integration test spins up a real database, a real message queue, or a real HTTP endpoint—but nothing more—and asserts that the emergent behaviour is correct.
Unit vs Integration Tests: The Real Distinction
It’s not about the number of classes involved. It’s about whether you’re testing a design assumption or a runtime assumption. Unit tests ask, “Does this function behave as specified?” Integration tests ask, “Does the serialisation layer round-trip correctly? Does the transaction actually commit? Does the downstream service respond with the schema we expect?”
In a .NET ecosystem, spinning up a WebApplicationFactory with an in-memory database (or a containerised Postgres instance) gives you a high-fidelity test that runs in under a second. That’s the sweet spot: fast enough to run on every push, realistic enough to catch the wiring bugs that unit tests miss.
End-to-End Tests: The Last Resort
End-to-end tests traverse the entire system, from the UI or API entry point down to the database and back. They’re the only test that tells you the system is actually assembled correctly. But they’re also flaky, slow, and expensive to maintain. Use them sparingly—no more than half a dozen critical journeys—and automate them to run in a staging environment, not on every PR.
If you’re spending more time debugging E2E test flakes than actual bugs, your strategy is inverted.
A Practical Split for Teams That Ship
For a typical back-end system at Silver Stack, we aim for a rough 70/20/10 split of effort, not count. That means:
- 70% of the testing effort goes into integration tests that cover service boundaries, persistence, and contract exchanges.
- 20% into unit tests for the complex, pure-logic pockets.
- 10% into end-to-end smoke tests that prove the critical paths hang together.
The numbers will shift if you’re building a library or a heavy algorithm. The principle stays: invest test effort where the risk of failure is high and the cost of fixing it late is painful.
Writing the Test Before the Bug
Adopt a simple rule: every non-trivial bug fix must be accompanied by a test that fails before the fix and passes after. This naturally builds your coverage in the most valuable areas—the places where things actually break. Over a few sprints, you’ll find your suite becomes a precise map of the system’s brittleness, rather than a blanket of meaningless assertions.
A testing strategy that actually ships treats tests as a product, not a chore. It’s okay to delete a test that no longer provides a signal. It’s okay to skip a test for code that changes shape weekly. The measure of a good suite is not its size, but how quickly it tells you the truth when you’ve broken something that matters.
If your current suite feels like a drag rather than a safety net, it’s worth revisiting how you’re splitting the work. Talk to us about your testing strategy →
READY TO IMPLEMENT THIS?
We specialize in turning these high-impact engineering concepts into production-grade systems.