The Shape of the Suite
The pyramid is advice about cost and feedback speed, not a rule about counts. What it is actually claiming, and where it is wrong.
Analysis
The test pyramid says: many fast narrow tests, fewer slow broad ones. It is widely cited, frequently misapplied, and worth restating in terms of what it is actually about.
What it is claiming
Not that unit tests are better. That narrow tests are cheaper to run, faster to fail, and easier to localise.
Cost per test execution rises as scope widens: a function call, then a process, then a whole environment.
Diagnostic value falls. A failing unit test names the function; a failing end-to-end test tells you something is broken somewhere.
Reliability falls. Broader tests have more moving parts and are consequently flakier.
So: put the volume where each run is cheap and each failure is informative. That is the whole argument, and it is about economics rather than purity.
Where it is wrong
Coverage of integration risk. Most production incidents come from interactions between components, and unit tests by construction do not exercise those.
A suite of thousands of unit tests can pass while the application is broken, and this is common enough that treating the pyramid as a target rather than a heuristic is a real risk.
Mocking hides the problem. A unit test with mocked collaborators verifies that the code calls the mock as expected, which is not the same as verifying it works.
Some architectures invert it. A service that is mostly integration — moving data between systems with little logic of its own — genuinely needs more integration testing than unit testing, and forcing a pyramid shape on it produces tests that verify nothing.
The useful reframing
Ask what each test costs and what it tells you, rather than what layer it belongs to.
Fast and specific: as many as useful.
Slow and broad: as few as necessary, chosen deliberately, covering the paths that matter most.
The middle — component or service-level tests with real dependencies where cheap, fakes where not — is frequently the most valuable band and the most neglected.
Signs the shape is wrong
The suite is slow and the unit tests are most of it, which usually means the unit tests are not actually narrow: they boot the application, hit a database, or load large fixtures.
End-to-end tests are the only ones anyone trusts, which means the narrow tests are testing mocks.
A change breaks fifty tests, which means the tests are coupled to structure rather than behaviour.
Nobody can say what a failing test means without opening it, which is a naming and scope problem.
Coverage is high and bugs still escape, which means the tests exercise lines without asserting behaviour.
What to do about it
Measure duration by category, not test count. Counts tell you nothing about cost.
Find the slow tests that are labelled as unit tests. There will be many, and they are misclassified rather than slow.
Cut end-to-end tests to the paths that would actually stop the business, and make those reliable rather than numerous.
Fill the middle deliberately, with real dependencies where they are cheap to run.
Stop counting. The number of tests is not a goal and reporting it invites the wrong behaviour.
Auditing the middle
The band that is usually thinnest and most valuable, and it is worth a deliberate look.
Find the tests that exercise a component with its real dependencies — a real database, a real message broker, no mocks for the things that are cheap to run.
Count them. In most suites the answer is very few.
Look at what the unit tests mock. Anything mocked in many tests is a candidate for a component-level test that exercises it properly.
Add there before adding end-to-end tests, which are slower and flakier for a similar increase in confidence.
Naming tests by behaviour
A change that costs nothing and improves triage more than most tooling.
A test name should say what behaviour broke, not which method was called.
"rejects an order with no line items" tells a developer what to look at; "testValidate3" does not.
Check the failure output: if a reader must open the test to know what failed, the name is wrong.
Rename as you touch them, rather than as a project.
This is the cheapest improvement available to failure triage, and it survives every refactor of the code underneath.