IAnswerable

The Test Pyramid Versus Reality

24 Mar 2026

The test pyramid says: lots of unit tests, fewer integration tests, a handful of end-to-end tests. For a small product, that ratio is often backwards from where your actual risk lives. Unit tests are cheap to write and cheap to be wrong about — they pass while the feature is broken because the seams between your units are exactly where bugs hide.

Where bugs actually happen in a small product

Not inside a single pure function. Between things: your API handler and your database query, your frontend and your API, your webhook handler and the third-party service that calls it. A function that formats a date correctly in isolation and a system where the date format breaks the invoice PDF are two different facts, and unit tests only tell you the first one.

If most of your codebase is glue — calling a database, calling an external API, rendering a response — then most of your bugs live in the glue, and a pyramid weighted toward isolated unit tests is testing the 20% of your code that was never going to break.

What actually pays off at small scale

Integration tests around your real boundaries. Spin up a real Postgres (see testcontainers), hit your actual API routes, assert on the actual response. This catches the class of bug — wrong SQL, a serialization mismatch, a missing migration — that a mocked-out unit test structurally cannot catch, because the mock is your assumption about the dependency, and the bug is usually in that assumption being wrong.

test('POST /orders creates an order and decrements stock', async () => {
  const res = await request(app).post('/orders').send({ sku: 'ABC', qty: 2 })
  expect(res.status).toBe(201)
  const stock = await db.query('SELECT qty FROM inventory WHERE sku = $1', ['ABC'])
  expect(stock.rows[0].qty).toBe(8)
})

One test like this covers the handler, the query, and the schema, in one assertion. Ten unit tests mocking the database would cover none of the actual risk — a wrong column name — because the mock doesn’t know the column name is wrong.

A small number of end-to-end tests on the paths that make you money. Signup, checkout, the one workflow your product exists to do. Not comprehensive coverage — five to ten flows, run against a real (or staging) deployment, that tell you “is the product fundamentally broken” before a customer does.

Unit tests where the logic is actually complex. Pricing calculations, permission checks, anything with real branching logic and no I/O. This is where the classic pyramid is right — cheap, fast, exhaustive tests for pure logic. Just don’t extend that reasoning to code that’s mostly a database call with a if statement wrapped around it.

What to skip

Don’t unit test a function that just calls another function and returns its result. Don’t write a unit test and an integration test for the same thin CRUD handler — pick the integration test, it’s the one that would actually fail if you broke it.

The shape that actually holds up

For a small product, aim for more of a diamond than a pyramid: fewer trivial unit tests, a solid middle layer of integration tests against real dependencies, a thin cap of end-to-end tests on revenue paths. It’s a smaller total number of tests than orthodox pyramid advice suggests, and it catches more real bugs, because it’s tests aimed at where your risk actually is instead of where tests are cheapest to write.

The pyramid isn’t wrong for a thousand-engineer org with slow, flaky E2E suites and a genuine need to push testing weight down. It’s wrong for a five-person team whose entire codebase is integration points. Test your actual risk, not the shape from the blog post.