Most teams don't have a "lack of testing" problem. There is a problem with poorly architected tests.
They start out well-intentioned. Someone writes one test, then another, and within a few months there is a suite with hundreds of cases. The problem appears when this suite starts taking twenty minutes to run, breaks due to any trivial change and no one trusts the red result anymore. The natural reaction is to turn off the "always failing" test. From then on, the suite becomes a theater.
This guide is straightforward: before discussing which tool to use, decide on the architecture of your tests. It is this decision that separates a suite that protects the team from one that slows it down.
Why architecture comes before coverage
Coverage is a metric that is easy to measure and easy to fool. You can have 90% coverage by testing getters and setters, without protecting any business rules that matter.
What really sustains speed is the way the tests are organized: what each level tests, how fast they run, how isolated they are and how clearly they point out what broke. This is an architectural decision, not a quantity decision.
I think of it as infrastructure. You don't scale a system by randomly stacking servers; defines layers, responsibilities and contracts. It's the same with tests. Without design, the suite grows as debt, not as an asset.
The pyramid is still the starting point
The testing pyramid remains the most useful mental model to start with. The basis is unit tests: many, fast, isolated. In the middle, integration tests, which check whether the pieces talk to each other. At the top, few end-to-end tests, which exercise user flow.
The rule of thumb is simple. The higher up the pyramid, the more expensive and slower the test, and the more fragile. That's why the top must be narrow. A team that inverts the pyramid, with dozens of interface tests and few unit tests, will have a slow and unstable suite.
The most common mistake here is to treat the pyramid as dogma. In systems that are very integration-oriented, APIs that orchestrate services, for example, it makes sense to thicken the integration layer. The format matters less than the principle: push verification to the cheapest level that still gives confidence.
Decisions that define the architecture
Some choices have a disproportionate impact on the health of the suite. It's worth resolving them early.
The first is what to insulate. Unit tests must run without a bench, without a network, without a real clock. When you need to go up half a system to test a function, the problem isn't the testing, it's the code coupling. The test is only reporting.
The second is how to deal with external dependencies. Mocks and stubs speed it up, but they lie: they test what you think the dependency does, not what it does. So I reserve real integration tests for critical boundaries, payment, authentication, persistence, and use doubles sparingly for the rest.
The third is where the tests live. Keep them close to the code they check. Suites in separate repositories almost always rot, because they change at a different pace than production code.
Speed is a feature of the suite
A slow suite is a suite that will be ignored. If running everything takes half an hour, the developer will only run part of it, or nothing at all, and only discover the problem on the CI conveyor belt, too late.
The practical goal is for the unity layer to run in seconds, locally, during development. This requires discipline: parallelizable tests, no dependency on shared state, no sleep to "wait" for something. Fixed waits are the biggest source of slowness and instability in real suites.
In CI, separate by stage. Run unit first, fail quickly, and only then move on to integration and end-to-end. There is no point in spending ten minutes on an interface test if a basic rule is already broken.
The real enemy: intermittent testing
If there is a single item that destroys trust in a suite, it is the test that sometimes passes and sometimes fails without anything changing. It's worse than having no test, because it teaches the team to ignore red.
Intermittent tests almost always arise from three sources: timing dependency, execution order dependency, and shared state across cases. Solving this is a work of architecture, not patience. Each test must set up and clean up its own scenario, without assuming anything about what has run before.
I treat intermittent testing as an incident, not as noise. When one appears, I either fix it or remove it. Keeping an unreliable test in the suite contaminates all the others.
Who guarantees the quality of the suite
Here comes the management vision that many teams ignore. Test code is code. It needs review, refactoring and ownership. An abandoned suite degrades at the same rate as any other unmaintained system.
In teams I lead, the rule is that testing is part of the definition of done, it is not a separate task pushed to the end of the sprint. And the health of the suite (execution time, intermittent failure rate) is included as an engineering metric, alongside others that we monitor.
In the public context and in products that deal with sensitive data, this takes on another weight. A reliable suite is part of what allows you to change a critical system without praying for anything to break. This is governance, not technical whimsy.
Start small, but start with drawing
If your suite is still small, this is the cheap time to get the architecture right. Define the levels, establish that unit tests do not touch infrastructure, require each test to be isolated and treat speed as a requirement.
If the suite is already big and painful, don't try to rewrite everything. Stop bleeding first: eliminate the intermittent ones, separate the stages in the IC and protect with new tests only what changes frequently. The rest you improve little by little.
Automated tests do not exist to prove that code works today. They exist to give you the courage to change it tomorrow. A good test architecture is, in essence, insurance against the fear of evolving the system.
If your team already has tests but has lost confidence in them, it's worth revisiting the architecture before writing another case. I have other texts on the blog about quality and software engineering, and if this is a real problem in your organization, this is the type of conversation that pays off.
Also read
- Automated test architecture: the essential steps to set up from scratch
- Regression testing: insurance against breaking what already worked
- Software testing cycle: trends and a quick guide for leaders
- Automated testing: why untested code is debt
- Software performance: what real cases teach about quality
- Automated Testing: Architecture and Fundamentals