Evaluating CodeRabbit? Same review, plus real test runs. See why

Automation Testing20 min read

Software Testing Pyramid: Layers and Variants

S
Technical Writer, Qodex
A three-layer pyramid: many unit tests, fewer integration tests, few end-to-end tests

The software testing pyramid is a guide for balancing automated tests. Keep many fast, narrow unit tests at the base, fewer integration and contract tests in the middle, and a small set of broad end-to-end tests at the top. Treat the shape and ratios as guidance, not law. Adjust the mix to your architecture, risks, feedback speed, and maintenance cost.

Qodex fills the upper layers for you: it writes API and UI scenarios, runs them on every pull request, and saves them as standard Playwright and HTTP code you own. See Qodex UI testing.

Software testing pyramid at a glance

Each layer buys a different kind of confidence at a different price. The table is the decision you make when a new check needs a home: what does it prove, what has to be running, and how soon do you want the answer.

Layer or modelWhat it provesTypical scopeFeedback speedAdd more when
UnitOne rule or branch behaves as specifiedA function or class, collaborators stubbedFast, nothing runningBranching logic carries the risk
Integration or serviceYour code works against a real boundaryA route or repository with the real database or queueSlower, still localBugs live in wiring and mapping
ContractConsumer and provider agree on request and responseOne consumer-provider pair, mock providerClose to integrationTwo teams release separately
API componentOne service honors its own HTTP contractThat service and its store, external calls stubbedFast for its coverageThe service is worth checking alone
API E2EA journey works through the deployed stackSeveral deployed servicesSlow, environment boundA flow crosses services
UI E2EA person can finish the flow in a browserBrowser and the full deployed stackSlow, and the most fragile hereThe journey earns or blocks revenue
Trophy (Kent C. Dodds)JavaScript behavior, weighted to integrationA component tree, mocked networkIntegration dominatesRisk sits between components
Honeycomb (Spotify)Microservice interactionsOne service and its collaborators, peers stubbedIntegration dominatesComplexity is in service calls

What each layer needs running and where it belongs in CI both follow from scope: the narrower the test, the less has to be alive for it and the earlier it can run.

Ratios such as 70% unit, 20% integration, and 10% end-to-end are illustrative. CircleCI calls 70/20/10 a common rule of thumb and adds that the exact ratio depends on the project (CircleCI, read 19 September 2026). No primary source makes that split part of the original model, so use it to picture the shape, never as a target to hit.

Where the testing pyramid came from

Mike Cohn introduced the test pyramid in Succeeding with Agile: Software Development Using Scrum, published by Addison-Wesley Professional in November 2009 (Mountain Goat Software, read 19 September 2026). His original three layers were Unit Tests, Service Tests, and User Interface Tests.

The account that gets cited in practice is The Practical Test Pyramid, written by Ham Vocke and published on martinfowler.com on 26 February 2018 (martinfowler.com, read 19 September 2026). Get the attribution right: the article lives on Martin Fowler's site, but Vocke wrote it. It restates Cohn's shape, then argues the layer names are the least useful part of the model.

Two ideas survive from the original: write tests at different levels of granularity, and write fewer of them as scope grows. The count of layers and what you call them is local detail.

Terminology helps here even though it does not decide anything. ISTQB defines component testing as "A test level that focuses on individual hardware or software components". It defines integration testing as "A test level that focuses on interactions between components or systems" (ISTQB glossary, read 19 September 2026). Both definitions describe scope. Neither mentions a tool, a framework, or a ratio. That is the point: a layer is a claim about how much of the system a test puts under load, not about what you wrote it with.

The pyramid is a model, not a standard. Nothing in Cohn's book page, the ISTQB glossary, or the ranking guides checked for this page mandates a ratio or a coverage percentage. Tests support evidence about quality; they do not prove compliance with any framework.

The three layers

Read the layers as scopes, from the smallest piece of code that can hold a bug to the whole running product. The layer is set by how much has to be alive for the test to run.

Base: unit tests

A unit test loads one function or class, feeds it values, and checks what comes back. Collaborators are stubbed so a failure points at one place. Pricing rules, validation, state machines, and anything with branches belong here: each branch is a cheap case rather than a deployment.

The base is wide because these tests are cheap enough to run on every save. They also carry the least evidence: a green unit suite says every piece behaved alone, and nothing about whether the pieces were wired together.

Stop adding here once a test asserts how the code works rather than what it does. A test that pins the order of private calls fails on a rename that changed no behavior, which is a maintenance bill with no payoff.

Middle: integration and service tests

The middle layer keeps your code and swaps only the far side of a boundary. A repository test runs against a real database. A route test runs the real HTTP handler, the real serializer, and the real error mapping, with the payment gateway stubbed. Vocke's rule is to keep the piece under test narrow while the dependency stays real, because that is where mapping and wiring bugs show up (martinfowler.com, read 19 September 2026).

This is the layer the arguments are about, because the same test can be described as a unit test with a database or a service test with stubs. For the boundary between the bottom and middle layers, compare unit testing vs integration testing with the same behavior tested both ways.

Two things belong here by default. The first is anything that crosses a serialization boundary, where field names and types quietly drift. The second is anything that depends on a real engine, such as a unique constraint or a transaction rollback.

Top: end-to-end tests

An end-to-end test drives the deployed system the way a user or a calling service would and asserts the outcome the business cares about. It is the only layer that proves the parts, the configuration, and the environment work together. It is also the most expensive to own: slow to run, sensitive to test data, and quick to turn flaky. A full treatment is in what is end-to-end testing.

Keep the top thin on purpose: a short list of journeys chosen by what loses money or blocks a release. When one fails, ask which narrower test could have caught the same bug, write that one, and keep the broad test only if it still adds confidence.

Flakiness is the tax at this layer. A suite people rerun until it goes green has stopped being evidence, so find and quarantine flaky tests early.

Where API and contract tests fit

"API test" names an interface, not a scope, which is why the question of where API tests belong keeps getting asked. The same HTTP request can sit at three different heights.

  • Narrow. The request goes to your route in memory, with every external call stubbed. That is a component or integration test that happens to speak HTTP.

  • Service level. The request goes to one running service with its own database, while its peers are stubbed. Broader than a unit test, far cheaper than a full journey.

  • End to end. The request goes to a deployed stack and travels through several services. Vocke's REST API end-to-end example is exactly this, and it sits at the top of the pyramid despite having no browser in it (martinfowler.com, read 19 September 2026).

The same holds for the UI. A component rendered with a mocked network is a narrow test, though it touches what a user sees. Playwright's documentation uses API calls for server-side tests and for setting up state around browser tests, the same interface at different scopes (Playwright, read 19 September 2026). Decide the layer by what has to be running.

Contract tests sit in the middle for a structural reason: they check that a consumer and a provider agree on the shape of requests and responses, without needing the full journey. Pact's own guidance says you can find out before you deploy whether your applications will work together, with no wait for slow end-to-end tests (Pact docs, read 19 September 2026). The consumer records what it sends and expects, the provider replays it, and both sides fail on their own pipeline when the agreement breaks.

Pact's docs also mark out where contract testing is the wrong tool. Their list is APIs whose other side will not use Pact, public APIs with no controlled consumer relationship, functional testing of the provider, and performance or load testing (Pact docs, read 19 September 2026). Contracts replace some end-to-end tests, not the provider's own tests. The setup and tooling are covered in contract testing.

Why ratios are guidance, not law

The 70/20/10 split gets quoted a lot and is grounded in nothing. CircleCI presents it as a common rule of thumb and immediately says the exact ratio depends on the project (CircleCI, read 19 September 2026). It is an illustration of a shape. Treat it as a picture, not a target, and never as something Cohn prescribed.

A better rule fits on one line: push each check to the lowest layer that can prove the behavior, and keep a broader test only when it adds confidence the lower one cannot. That rule survives contact with any architecture, which is more than a fixed ratio can say.

What actually moves the mix:

  • Architecture. A service that mostly calls other services has thin logic and fat boundaries, so the middle grows. A pricing or rules engine is the reverse.

  • Where your bugs come from. Read your recent incidents, as far back as the record is honest. If they were wiring, configuration, and contract drift, more unit tests will not help.

  • Feedback time. If the fast stage no longer fits the time developers will wait, the shape is wrong whatever the percentages say.

  • Flake rate and maintenance. Tests that get rerun or skipped are not coverage. Count them as a cost against the layer they sit in.

Measure the shape you have rather than arguing about the one you want: tests per layer, runtime per layer, and failures that caught a real bug.

Testing trophy and honeycomb alternatives

Two well-known variants change the weighting for a specific context. Neither is a universal replacement for the pyramid, and their authors did not present them as one.

The testing trophy comes from Kent C. Dodds, who writes that he created it as a general guide to the return on investment of different forms of testing for JavaScript applications (kentcdodds.com, published 13 July 2019, read 19 September 2026). It puts static checks such as typing and linting at the bottom, then unit, then a wide integration band, then end-to-end at the tip. His argument is that integration tests give the best balance of confidence against cost in that setting, and that types and lint rules already remove a class of bugs before any test runs.

The trophy fits a frontend-heavy codebase where most risk sits between components: a form bound to state bound to a request, rendered as a tree. Testing those pieces in isolation proves little, because the bugs live in the seams. It fits less well where the risk is a branching rule with many cases, which is still cheapest to cover one case at a time.

The testing honeycomb comes from Spotify's engineering team and targets microservices. It says to focus on integration tests, keep a few implementation-detail tests, and have even fewer integrated tests, ideally none (Spotify Engineering, published 11 January 2018, read 19 September 2026). Their vocabulary matters: an integrated test is one that passes or fails based on the correctness of another system, which is the kind they want to avoid because the failure is not yours to fix.

The honeycomb suits a service whose own logic is small and whose risk is the calls it makes and serves. Pair it with contract tests and the middle layer does most of the work, with almost nothing depending on another team's deployed code.

All three shapes agree on more than they disagree: prefer narrow, fast tests, keep broad tests few, and choose by where the bugs live. Where that is depends on your system, not on the model.

Anti-patterns and what sits outside the pyramid

The failure with a name of its own is the ice cream cone: the shape upside down, a mass of slow end-to-end tests on a thin base. Vocke warns that such a suite becomes a nightmare to maintain and takes far too long to run (martinfowler.com, read 19 September 2026). It arrives by accident: every bug gets a browser test, and nobody deletes anything.

Duplicated coverage is the quieter cost. A rule checked at every layer gives you a failure per layer for one bug, and a file per layer to update for one change. When a broad test and a narrow test prove the same thing, keep the narrow one.

Over-mocking hollows out the middle. Stub the database, the queue, and the clock, and the test passes against your idea of how those behave instead of how they behave. Once every boundary is faked, the layer proves nothing an assertion on the mock would not.

Counting instead of deciding. A coverage percentage is not a shape and does not say whether the right things are checked. Track runtime per layer and which layer caught each real defect.

Some work sits outside the model. Exploratory testing, where a person looks for what nobody specified, has no layer. Performance, load, security, and accessibility are their own activities. The pyramid organizes automated functional checks, not the whole of quality.

Apply the pyramid to a runnable checkout suite

Here is the model as code. One flow, POST /orders, checked at five heights, one test per layer, so the difference between layers is the only thing on show. Every file below was copied into an empty directory and run on 19 September 2026. In our run the interpreter was Node 26.9.0, all five layers passed, and the output at the end is that run's output. Durations depend on the machine.

Versions are the current npm releases on 19 September 2026. They are Vitest 5.0.1 (MIT, Node ^22.12.0 or ^24.0.0 or 26 and newer) and Supertest 7.2.2 (MIT, Node 14.18 and newer). The other two are Pact JS 17.1.4 (MIT, Node 22 and newer) and Playwright Test 1.63.0 (Apache-2.0, Node 20 and newer). CI minutes, browsers, and environments still cost money wherever you run them.

checkout/
  package.json
  playwright.config.js
  public/checkout.html        the page the browser test drives
  src/price.js                discount, tax and rounding rules
  src/orders.js               order service and its repository boundary
  src/payments.js             HTTP client for the payment provider
  src/server.js               routes and JSON handling
  src/main.js                 starts the service
  tests/support/payment-stub.js   stand-in provider for the two end-to-end runs
  tests/unit/price.test.js
  tests/integration/orders.test.js
  tests/contract/payments.test.js
  tests/api-e2e/orders.spec.js
  tests/ui-e2e/checkout.spec.js

The production code is four small files.

// src/price.js
export const TAX_RATE = 0.2;

export function priceOrder({ unitPriceCents, quantity, discountCode }) {
  if (!Number.isInteger(quantity) || quantity < 1) {
    throw new Error('quantity must be a positive integer');
  }
  const subtotal = unitPriceCents * quantity;
  const discount = discountCode === 'SAVE10' ? Math.round(subtotal * 0.1) : 0;
  const total = Math.round((subtotal - discount) * (1 + TAX_RATE));
  return { subtotal, discount, total };
}
// src/orders.js
import { priceOrder } from './price.js';

export function inMemoryOrders() {
  const rows = [];
  return {
    save(order) {
      const row = { id: 'ord_' + (rows.length + 1), ...order };
      rows.push(row);
      return row;
    }
  };
}

export function createOrderService({ payments, orders }) {
  return async function createOrder(request) {
    let price;
    try {
      price = priceOrder(request);
    } catch (error) {
      return { status: 400, body: { error: 'INVALID_ORDER', message: error.message } };
    }
    const payment = await payments.charge({ amountCents: price.total, token: request.token });
    if (payment.status !== 'approved') {
      return { status: 402, body: { error: 'PAYMENT_DECLINED' } };
    }
    return { status: 201, body: orders.save({ ...price, paymentId: payment.id }) };
  };
}
// src/server.js
import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';

const CHECKOUT_PAGE = readFileSync(new URL('../public/checkout.html', import.meta.url));

export function buildApp(createOrder) {
  return createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/checkout') {
      res.writeHead(200, { 'content-type': 'text/html' });
      return res.end(CHECKOUT_PAGE);
    }
    if (req.method !== 'POST' || req.url !== '/orders') {
      res.writeHead(404, { 'content-type': 'application/json' });
      return res.end(JSON.stringify({ error: 'NOT_FOUND' }));
    }
    let raw = '';
    req.on('data', (chunk) => { raw += chunk; });
    req.on('end', async () => {
      const result = await createOrder(JSON.parse(raw));
      res.writeHead(result.status, { 'content-type': 'application/json' });
      res.end(JSON.stringify(result.body));
    });
  });
}
// src/payments.js
export function httpPayments(baseUrl) {
  return {
    async charge({ amountCents, token }) {
      const response = await fetch(baseUrl + '/charges', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ amountCents, token })
      });
      return response.json();
    }
  };
}

Two more files start the thing: src/main.js and the page the browser test drives.

// src/main.js
import { buildApp } from './server.js';
import { createOrderService, inMemoryOrders } from './orders.js';
import { httpPayments } from './payments.js';

const createOrder = createOrderService({
  payments: httpPayments(process.env.PAYMENTS_URL),
  orders: inMemoryOrders()
});

buildApp(createOrder).listen(Number(process.env.PORT));
<!doctype html>
<title>Checkout</title>
<button id="pay">Pay</button>
<p id="result"></p>
<script type="module">
  document.querySelector('#pay').addEventListener('click', async () => {
    const response = await fetch('/orders', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ unitPriceCents: 2500, quantity: 2, token: 'tok_ok' })
    });
    const body = await response.json();
    document.querySelector('#result').textContent =
      response.status === 201 ? 'Order ' + body.id + ' confirmed' : 'Payment declined';
  });
</script>

The unit test proves one rule, that the discount is applied before tax and the result is rounded to whole cents. Nothing is running. It fails on bad arithmetic and on nothing else.

// tests/unit/price.test.js
import { describe, it, expect } from 'vitest';
import { priceOrder } from '../../src/price.js';

describe('priceOrder', () => {
  it('applies the discount before tax and rounds to whole cents', () => {
    expect(priceOrder({ unitPriceCents: 2500, quantity: 3, discountCode: 'SAVE10' }))
      .toEqual({ subtotal: 7500, discount: 750, total: 8100 });
  });
});

The integration test proves the route, the JSON handling, the pricing call, and the repository work together. The payment gateway is stubbed, because the point of this layer is your own wiring.

// tests/integration/orders.test.js
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { buildApp } from '../../src/server.js';
import { createOrderService, inMemoryOrders } from '../../src/orders.js';

const payments = { charge: async () => ({ status: 'approved', id: 'pay_1' }) };
const app = buildApp(createOrderService({ payments, orders: inMemoryOrders() }));

describe('POST /orders', () => {
  it('prices the order, records it and returns 201 with the order id', async () => {
    const response = await request(app)
      .post('/orders')
      .send({ unitPriceCents: 2500, quantity: 2, token: 'tok_ok' });

    expect(response.status).toBe(201);
    expect(response.body).toEqual({
      id: 'ord_1', subtotal: 5000, discount: 0, total: 6000, paymentId: 'pay_1'
    });
  });
});

The contract test proves the consumer and the payment provider agree on the request and the response. Pact runs a mock provider, the client sends a real request to it, and the recorded pact is what the provider verifies on its own pipeline.

// tests/contract/payments.test.js
import { describe, it, expect } from 'vitest';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { httpPayments } from '../../src/payments.js';

const provider = new PactV3({
  consumer: 'checkout-service',
  provider: 'payments-api',
  dir: './pacts'
});

describe('payments contract', () => {
  it('sends the amount and token the provider expects, and reads the approval back', async () => {
    provider.addInteraction({
      states: [{ description: 'the token tok_ok can be charged' }],
      uponReceiving: 'a charge for 6000 cents',
      withRequest: {
        method: 'POST',
        path: '/charges',
        headers: { 'content-type': 'application/json' },
        body: { amountCents: 6000, token: 'tok_ok' }
      },
      willRespondWith: {
        status: 200,
        headers: { 'content-type': 'application/json' },
        body: { status: 'approved', id: MatchersV3.string('pay_1') }
      }
    });

    await provider.executeTest(async (mockServer) => {
      const payment = await httpPayments(mockServer.url).charge({ amountCents: 6000, token: 'tok_ok' });
      expect(payment.status).toBe('approved');
    });
  }, 30000);
});

The API end-to-end test proves a declined payment surfaces as a 402 through the deployed service. No browser, and still the top of the pyramid, because the whole stack has to be running for it to pass.

// tests/api-e2e/orders.spec.js
import { test, expect } from '@playwright/test';

test('a declined card returns 402 through the deployed service', async ({ request }) => {
  const response = await request.post('/orders', {
    data: { unitPriceCents: 2500, quantity: 2, token: 'tok_declined' }
  });

  expect(response.status()).toBe(402);
  expect(await response.json()).toEqual({ error: 'PAYMENT_DECLINED' });
});

The UI end-to-end test proves a person can finish the flow in a browser. It is the only test here that would catch a button wired to nothing.

// tests/ui-e2e/checkout.spec.js
import { test, expect } from '@playwright/test';

test('a shopper pays and sees the order confirmed', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay' }).click();

  await expect(page.locator('#result')).toHaveText('Order ord_1 confirmed');
});

The two end-to-end runs need a payment provider to talk to. This one stands in for it, approving tok_ok and declining everything else, so the suite needs no account anywhere.

// tests/support/payment-stub.js
import { createServer } from 'node:http';

createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/') {
    // Playwright polls this while it waits for the process to come up.
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ ok: true }));
  }
  if (req.method !== 'POST' || req.url !== '/charges') {
    res.writeHead(404, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ error: 'NOT_FOUND' }));
  }
  let raw = '';
  req.on('data', (chunk) => { raw += chunk; });
  req.on('end', () => {
    const { token } = JSON.parse(raw);
    const approved = token === 'tok_ok';
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify(
      approved ? { status: 'approved', id: 'pay_1' } : { status: 'declined' }
    ));
  });
}).listen(Number(process.env.PORT));

Playwright starts both processes for the two end-to-end runs: the stand-in payment provider, then the service pointed at it.

// playwright.config.js
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: { baseURL: 'http://127.0.0.1:4050' },
  webServer: [
    {
      command: 'node tests/support/payment-stub.js',
      env: { PORT: '4051' },
      url: 'http://127.0.0.1:4051',
      reuseExistingServer: false
    },
    {
      command: 'node src/main.js',
      env: { PORT: '4050', PAYMENTS_URL: 'http://127.0.0.1:4051' },
      url: 'http://127.0.0.1:4050/checkout',
      reuseExistingServer: false
    }
  ]
});

The scripts give each layer its own command and one command that runs them in order, fastest first.

{
  "name": "checkout-pyramid",
  "private": true,
  "type": "module",
  "scripts": {
    "test:unit": "vitest run tests/unit",
    "test:integration": "vitest run tests/integration",
    "test:contract": "vitest run tests/contract",
    "test:api-e2e": "playwright test tests/api-e2e",
    "test:ui-e2e": "playwright test tests/ui-e2e",
    "test:pyramid": "npm run test:unit && npm run test:integration && npm run test:contract && npm run test:api-e2e && npm run test:ui-e2e"
  },
  "devDependencies": {
    "@pact-foundation/pact": "17.1.4",
    "@playwright/test": "1.63.0",
    "supertest": "7.2.2",
    "vitest": "5.0.1"
  }
}

Copy the files above into an empty directory, run npm install, then npx playwright install chromium for the browser test, then npm run test:pyramid. Use npm ci instead once you have committed the lockfile that npm install writes. The output below is from that run, trimmed to the lines that report results.

> test:unit
> vitest run tests/unit
 Test Files  1 passed (1)
      Tests  1 passed (1)
> test:integration
> vitest run tests/integration
 Test Files  1 passed (1)
      Tests  1 passed (1)
> test:contract
> vitest run tests/contract
 Test Files  1 passed (1)
      Tests  1 passed (1)
> test:api-e2e
> playwright test tests/api-e2e
  1 passed (1.1s)
> test:ui-e2e
> playwright test tests/ui-e2e
  1 passed (2.4s)

One test per layer is the teaching version, not a ratio. In a real service the base carries a case per branch, the middle a test per boundary, and the top carries only the journeys you would stop a release for. Growing that into something you maintain is covered in what is an automated test suite.

Put the suite into CI

Order the pipeline by feedback speed and scope, not by what the test types are called. Vocke makes the same point: let the stages follow how fast a check is and how much it covers (martinfowler.com, read 19 September 2026).

For the suite above that means unit and integration on every push. Contract comes next, so a broken agreement stops the build before anything is deployed. The API and UI end-to-end runs come last, against a deployed environment. Fail fast at each stage so a broken rule never costs a browser run.

Record two numbers per stage from day one: runtime, and how often the stage failed for a reason that was not the product. The first says when the fast stage stopped being fast, the second says which tests to fix or delete. Rebalance from those numbers, not from a ratio. Where the checks sit in a wider delivery pipeline is covered in CI/CD testing.

The short version

The pyramid is one rule with a picture attached: prove each behavior at the lowest layer that can prove it, and keep a broader test only when it adds confidence the narrow one cannot. Ratios, layer names, and the choice between pyramid, trophy, and honeycomb are local decisions about where your bugs live. Measure runtime and failures per layer, then let the shape follow the evidence.

Frequently Asked Questions

What are the three layers of the software testing pyramid?

Unit tests at the base, integration or service tests in the middle, end-to-end tests at the top. Cohn's original labels were Unit Tests, Service Tests, and User Interface Tests. The layer is set by scope: how much has to be running for the test to pass, not which library wrote it.

Who created the testing pyramid?

Mike Cohn introduced it in Succeeding with Agile: Software Development Using Scrum, published in November 2009 by Addison-Wesley Professional. The version that gets cited in practice is The Practical Test Pyramid, written by Ham Vocke and published on martinfowler.com on 26 February 2018. The article is hosted on Fowler's site but written by Vocke.

Is 70/20/10 the ideal testing pyramid ratio?

No. CircleCI calls 70% unit, 20% integration, and 10% end-to-end a common rule of thumb and says the exact ratio depends on the project (read 19 September 2026). No primary source ties that split to Cohn's model. Use it to picture the shape, then set your own mix from your architecture and your recent bugs.

Where do API tests fit in the testing pyramid?

At whatever height the test runs, because API names an interface, not a scope. A request to your route in memory is narrow. A request to one running service with peers stubbed sits in the middle. A request through a deployed stack is end-to-end, with or without a browser.

Where do contract tests fit in the testing pyramid?

In the middle. They check that a consumer and a provider agree on request and response shape without running the full journey. Pact says you can find out before deploying whether applications will work together, with no wait for slow end-to-end tests (read 19 September 2026). They replace some broad tests, not the provider's own tests.

What is the difference between the testing pyramid and testing trophy?

The trophy, from Kent C. Dodds, adds static checks such as typing and linting at the base and puts the most weight on integration tests for JavaScript applications, as a guide to return on investment (read 19 September 2026). The pyramid puts the most weight on unit tests. Both keep end-to-end tests few.

When should a microservices team use the testing honeycomb?

When the risk sits in service-to-service calls rather than inside one service. Spotify's honeycomb says to focus on integration tests, keep a few implementation-detail tests, and have even fewer integrated tests, ideally none (read 19 September 2026). Pair it with contract tests so little depends on another team's deployed code.

Is the software testing pyramid outdated?

Its ratios and layer names have aged; its rule has not. Prove each behavior at the lowest layer that can prove it, and keep broad tests few. Tooling has made some middle-layer tests cheaper than in 2009, which shifts the weighting. That is a change in the mix, not a reason to drop it.

Ship continuously. Test continuously.

Qodex explores your app, writes runnable tests, and replays them on every change at zero LLM cost.