1. Home
  2. Blog
  3. Software Testing

Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid is a simple picture of how to spread automated tests: many fast ones at the bottom, a few slow ones at the top. Here is what each layer is for, why the shape matters, and when a different shape fits better.

9 min read
The test pyramid explained: layers, ratios and when to break the shape

Key takeaways

  • The pyramid has many unit tests, fewer integration tests and very few end-to-end tests.
  • Lower layers are faster, cheaper and more precise. Higher layers give more confidence that the whole system works.
  • An upside-down suite, the ice cream cone, is slow, flaky and expensive to maintain.
  • Ratios such as 70/20/10 are rough guides. Your architecture should decide the exact shape.

A team automates its regression suite entirely through the browser. At first it feels like real progress, because the tests click through the app just as users do. A year later, the suite takes three hours, fails randomly, and nobody trusts it. The test pyramid is the model that would have warned them.

This article is a strategy guide, so it contains no code that we ran. The ratios quoted are common rules of thumb from industry sources, not measured results.

What is the test pyramid?

The test pyramid is a model for distributing automated tests across levels. Mike Cohn described it in the 2009 book Succeeding with Agile, and Martin Fowler’s writing later made it widely known. The original pyramid had unit tests at the base, service tests in the middle and UI tests at the top. Today the layers are usually called unit, integration and end-to-end.

The shape carries the message. The base is wide because you want many tests there. The top is narrow because you want only a few.

Why a pyramid?

Property Unit Integration End-to-end
Speed Milliseconds Seconds Seconds to minutes
Cost to write and maintain Low Medium High
Flakiness risk Very low Medium Higher
Failure points to One function A boundary between parts Somewhere in the system
Confidence the product works Narrow Moderate Broad

Each step up the pyramid trades speed and precision for realism. You need some realism, so the top layer exists. But you want most of your checking done where it is fast and where a failure points straight at the cause.

Unit tests: the base

A unit test checks one small piece of logic, such as a function or class, in isolation. It runs in memory, with slow or unpredictable dependencies replaced by test doubles, as covered in our mocking guide. Thousands can run in seconds, so developers run them on every change.

Good candidates are business rules, calculations, validation, parsing and edge cases. A discount rule with ten boundary values should be tested ten times at this level, not ten times through a browser. Our boundary value analysis guide shows how to choose those values.

Integration tests: the middle

Integration tests check that pieces work together: your code with a real database, a service with its message queue, or two services through their API. They catch problems unit tests cannot, such as a wrong SQL query, a missing database column, a serialization mismatch or a misconfigured connection.

API tests usually live here. Testing an endpoint directly, as in our API testing guide, checks real request handling and storage without the cost of driving a UI. Contract tests, which check that a service still meets the expectations of the services that call it, are often placed in this layer too.

The line between unit and integration tests is discussed in more depth in unit vs integration testing.

End-to-end tests: the top

End-to-end tests drive the whole system the way a user does, usually through a browser with a tool such as Playwright, Cypress or Selenium. They prove that the critical journeys work: sign up, log in, search, pay, receive a confirmation.

Keep them for exactly that. A small set of high-value journeys, run on every deployment, gives broad confidence. Trying to test every rule and edge case through the UI is what turns the pyramid upside down. Our end-to-end testing best practices cover keeping this layer stable.

What ratio is right?

A common rule of thumb is roughly 70% unit, 20% integration and 10% end-to-end, and some sources suggest 60-70% at the base. Treat these numbers as a starting point, not a target to enforce. Counting tests is less useful than asking two questions:

  • Is each behaviour tested at the lowest level that can catch its bugs?
  • Does the suite give fast, trustworthy feedback?

A logic-heavy library might be 95% unit tests. A thin web app that mostly moves data between a form and a database might rightly have more integration tests than unit tests, because that is where its bugs live.

Tip: When an end-to-end test catches a bug, ask whether a lower-level test could have caught it. If so, add that test. Over time, this pushes coverage down the pyramid and keeps the top small.

The ice cream cone anti-pattern

The inverted shape has a name: the ice cream cone. It has a big scoop of manual testing on top, lots of UI automation, a few integration tests and almost no unit tests. It usually grows when automation is added late, by a separate team, through the only interface they can reach: the UI.

The symptoms are familiar. Builds take hours, so feedback arrives a day later. Tests fail for reasons unrelated to the change, such as timing or test data, so people rerun them until they pass. Each UI change breaks dozens of tests. Eventually the team ignores red builds, and the suite stops protecting anything. Many of these failures are the flaky tests problem in disguise.

The testing trophy and honeycomb

The pyramid is not the only model:

  • The testing trophy, described by Kent C. Dodds for front-end apps, puts static analysis (type checking and linting) at the base and makes integration tests the largest layer. The reasoning is that component-level integration tests give the best return in UI code.
  • The testing honeycomb, described by engineers at Spotify for microservices, also emphasises integration tests, because a small service has little isolated logic and most risk sits in its interactions.

These models do not contradict the pyramid’s core idea. They all say: prefer the fastest test that gives real confidence, and keep slow, broad tests few. They differ on where that sweet spot is for a given architecture.

Where manual testing fits

The pyramid describes automated tests. Human testing still matters, but for different work: exploratory testing, usability, visual judgement and investigating new features. Automate the checks that must be repeated every release, and spend human time where curiosity and judgement find problems that scripts cannot.

Rebalancing an upside-down suite

  1. Measure. Count tests per layer and record run time and flaky failure rate.
  2. Stop the growth. New logic gets unit tests first; new UI tests need a clear reason.
  3. Push down. For each slow UI test, identify the rules it checks and cover them with unit or API tests.
  4. Prune. Delete UI tests whose checks are now covered lower down. Keep the critical journeys.
  5. Fix testability. Hard-to-unit-test code is often a design problem, such as logic mixed into UI handlers. Refactoring it makes the pyramid possible.

The pyramid in your CI pipeline

The layers map naturally onto pipeline stages in CI/CD. Run unit tests on every commit, since they finish in seconds or a few minutes. Run integration tests on every pull request. Run the small end-to-end suite before or just after deployment. Fast layers first means most problems are caught within minutes, and the expensive checks run only on changes that have already passed the cheap ones.

Measuring your pyramid’s shape

You cannot rebalance what you cannot see. Tag each test with its layer so you can count and run layers separately. In pytest, for example, register markers and select them on the command line (an illustration, not run for this article):

# pytest.ini
[pytest]
markers =
    unit: fast, isolated tests
    integration: tests that use a database or service
    e2e: browser journeys

# run one layer
pytest -m unit
pytest -m "integration or e2e"

Folder structure works too, such as tests/unit, tests/integration and tests/e2e. Then track a few numbers over time for each layer: how many tests, how long the layer takes, and how often its tests fail without a real bug. Most CI tools report durations, and pytest’s --durations=10 option lists the slowest tests. A layer whose run time keeps climbing, or whose flaky failure rate rises, is where to invest next.

Final thoughts

The test pyramid is a reminder to test each behaviour at the cheapest level that can catch its bugs. Build a broad base of unit tests, a solid middle of integration and API tests, and a thin top of end-to-end journeys. Adjust the exact shape to your architecture, but if your suite looks like an ice cream cone, start pushing tests down today.

Frequently asked questions

What is the test pyramid?

It is a model for automated testing with many unit tests at the base, fewer integration tests in the middle and very few end-to-end tests at the top. Mike Cohn introduced it.

What is a good ratio of unit, integration and E2E tests?

A common guide is about 70% unit, 20% integration and 10% end-to-end. Treat it as a starting point and adjust it to where your system’s risks are.

What is the ice cream cone anti-pattern?

It is an inverted pyramid with mostly manual and UI tests and few unit tests. It leads to slow, flaky and expensive test suites.

What is the testing trophy?

A model for front-end apps that places static analysis at the base and gives integration tests the largest share, arguing they offer the best confidence for the cost.

Is the test pyramid still relevant?

Yes. Its core idea, test at the fastest level that gives real confidence, applies everywhere, even when the exact shape differs by architecture.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →