1. Courses
  2. Engineering Roles
  3. Test Automation and Quality Engineering
  4. Stop 1

35 minIntermediate

A test strategy, not a pile of scripts

A suite of automated tests helps a team only when it is fast, stable and aimed at the risks that matter. This lesson gives you a method to decide which checks to automate, at which level, and how many minutes the whole suite may cost, using Mangrove Market's checkout as the running example.

By the end you can

  • Name the four ways a test suite fails a team, and calculate how flaky tests add up
  • Compare the test pyramid, the testing trophy and the honeycomb, and say what each level costs and catches
  • Push each checkout risk down to the fastest level that can catch it, and decide what not to automate
  • Set a budget in CI minutes and track suite time, flake rate and escaped defects
  • Write a one-page test strategy for a real feature

The everyday picture

Think about how a good restaurant kitchen controls quality. The cook tastes the sauce at the stove, many times a night. It takes two seconds, and when it is wrong, the cook knows exactly which pot to fix. At the pass, the head chef looks at every finished plate: is the sauce on the right dish, is the food hot, is the side there? That takes longer, and it catches problems no single cook can see. Once a month, the owner sends a mystery diner who books a table, orders a full meal and pays. That costs a whole meal and a week to get the report, and when the report says "dinner was disappointing", nobody knows which station caused it.

No sensible kitchen replaces tasting with a hundred mystery diners. It would be slow, expensive, and every vague report would start an argument. A kitchen with only tasting would still serve cold food on the wrong table. A test strategy is the same choice for software: many fast, precise checks close to the code, a few broader checks where the pieces meet, and a small number of full journeys that see what the customer sees.

Why test suites fail teams

Most teams that start automating do it the obvious way: take the manual test cases and turn each one into a script that clicks through the app. A year later, the suite often has four problems at once.

  • It is slow. A suite that takes 45 minutes does not run on every pull request. People batch their changes, so when it fails, five changes are suspects instead of one.
  • It is flaky. A turns red with no bug. After a few false alarms, people press "re-run" without reading the failure.
  • It tests the wrong things. Hundreds of checks that the login page has a logo, and none for the discount rule that loses money when it is wrong.
  • Nobody trusts red. This is where the first three lead. When a red build is usually noise, a real failure gets merged too. The suite still costs money to run, and it protects nothing.

Flakiness grows faster than people expect, because every test gets its own chance to fail. If a test flakes in 1 run out of 100, a single test is fine. But a run is green only when every test passes, so the chance of a false red is 1 - (1 - p) ** n for n tests that each flake with probability p. With 50 such tests, about 39.5% of runs fail with no bug. With 200 tests, about 87% do. The odds multiply, so a large suite of slightly flaky end-to-end tests is red most of the time. Discipline alone cannot beat that arithmetic. You fix it with fewer tests at the flaky level and more at levels that do not flake.

Quick check

A team has 100 end-to-end tests, each flaking about 1% of the time. Roughly how often is a run red with no real bug?

What each level costs and what it catches

A is about scope: how much of the real system one test runs. Mangrove's checkout has a customer app (React Native), an order service (NestJS and PostgreSQL), and a payment provider for cards and mobile wallets. Errors, debugging and testing introduced unit and integration tests; this lesson is about choosing between the levels.

  • Static checks: TypeScript and a linter read the code without running it. They catch a wrong type or a missing argument in seconds, and never flake.
  • Unit tests: call one function, such as priceOrder(lines, customer), with no network and no database. About a millisecond each. A failure names the exact rule that broke.
  • API tests (a kind of ): start the order service with a real PostgreSQL and call POST /orders over HTTP. Roughly 50 to 200 milliseconds each. They catch validation, SQL, transactions and the JSON the service returns. Unit and API tests builds them.
  • Contract tests: check that the app and the order service still agree on the shape of requests and responses, without running both together. Contract tests covers them.
  • End-to-end (E2E) tests: an drives the real app in a browser or on a device, with the real services behind it. Often 20 to 60 seconds each. Only this level sees that the button, the API and the payment page work together. End-to-end tests with Playwright builds them.
LevelRough time eachWhen it fails, it points atMain cost
Staticseconds, whole repoa file and linealmost none
Unit~1 msone rule in one functionbreaks when you change a function's design
API~50–200 msone endpoint and its dataa real database and clean test data
Contract~1 sone field two teams disagree ona pact file and a shared broker
E2E~20–60 s"something in the journey"environments, waits, flakiness, upkeep

Read the table as a trade. Going up a level buys confidence that real pieces work together. You pay for it in speed, in isolation (an E2E failure could be the app, the service, the test data or the network), and in maintenance, because broad tests break whenever any part they touch changes. A bug caught by a unit test costs a millisecond and points at one line. Caught by an E2E test, it costs 40 seconds, sometimes a false alarm, and twenty minutes of digging to find the layer.

Pyramid, trophy, honeycomb

People draw the ideal mix of levels as a shape. Three shapes come up in almost every team discussion.

The comes from Mike Cohn, who described it in his 2009 book Succeeding with Agile: a wide base of unit tests, a middle layer of service tests, and a thin top of UI tests. Its argument is cost. UI tests are slow, brittle and expensive to write, so keep them few, and get most of your checking from the cheap layer.

The testing trophy, which Kent C. Dodds drew in 2018, adds static checks as the base and makes integration tests the widest part, with a smaller unit layer and a small E2E top. His argument is confidence: tests that use your code the way users do, with fewer mocks, tell you more per test. It grew up in front-end work.

The honeycomb came from Spotify's engineering blog in 2018, for microservices. Most tests should be integration tests that run one service through its API with a real database. A few "implementation detail" tests cover code with its own complex logic. And, ideally, no "integrated" tests, meaning tests that pass or fail depending on whether another service is correct.

The shapes disagree less than they seem. Martin Fowler pointed out in 2021 that much of the argument is about what "unit" means: a test of one function that also runs its real helpers is a unit test to some people and an integration test to others. All three agree on two things: end-to-end tests stay few, and fast tests inside one deployable piece do most of the work. So pick a level for each risk, and let the shape appear by itself. Mangrove's price rules are pure logic with many combinations, so that part of checkout looks like a pyramid. The order service is mostly validation, SQL and wiring, so that part looks like a honeycomb.

Quick check

What do the pyramid, the trophy and the honeycomb all agree on?

E2E Service Unit E2E Integration Unit Static Integrated Integration Impl. detail Pyramid Cohn, 2009 Trophy Dodds, 2018 Honeycomb Spotify, 2018 Wider band = more tests
Three shapes for the same question. The pyramid puts most tests at the unit level, the trophy and the honeycomb at the integration level. The honeycomb's dashed top, integrated tests that depend on another service being correct, should ideally be empty. All three keep end-to-end tests few.

Choosing a level for each risk

Start from risks. A risk is something that could go wrong and would hurt: a wrong price, a double charge, an order lost between the app and the service. For each one, ask where the bug would live, then apply one rule: push each check down to the fastest level that can catch the bug. If the bug can only exist in a pure function, a unit test can catch it, and a slower test adds nothing. If the bug needs the database or the HTTP layer, go up one level. Go to E2E only when the bug needs the real app, the real services and the real network together.

Here is how that plays out for Mangrove's checkout rules. All money is in integer cents (Working with data explains why).

  • Delivery fee: 150 cents under a 2,500-cent cart, free from 2,500. This is arithmetic, so it gets unit tests at the boundaries: 2,499 and 2,500 (Designing tests that find bugs shows how to pick them).
  • FRESH10: 10% off from 2,000 cents, first order only, never together with the 5% staff discount. Three conditions make many combinations. A unit test table covers them all in milliseconds; through the UI, each combination would cost a minute.
  • Quantity 1 to 20 and a cart of 500 to 50,000 cents: the rule itself is a unit test. Whether POST /orders really answers 400 for qty: 21 depends on NestJS validation in the HTTP layer, so one or two API tests check that wiring.
  • Double tap on Pay: two identical requests must create one order. Only a real database with a unique key can prove it, so it is an API test.
  • A renamed field: if the service renames deliveryFeeCents, app versions already on customers' phones break. That is an agreement between two things released separately: a contract test.
  • Pay by wallet and see the confirmation: the app, the service and the payment provider's redirect all have to work together. That is one of a few E2E journeys.

The E2E layer then holds a short list of critical journeys: the paths that make money or lose trust if they break, such as sign in, add to cart, pay by card, wallet or cash on delivery, and track the order. Each journey checks the wiring once. The rules inside it are already covered below, so the journey does not need to try every discount.

Quick check

A tester wants to check that FRESH10 is refused when a staff discount is already applied. Which level fits best?

Clear right answer? Bug in pure logic? Inside one service? Two apps, two releases? E2E journey (few) Explore by hand Unit test API test Contract test no yes yes yes yes no no no
Ask the questions from the cheapest level up and stop at the first "yes". A check reaches the end-to-end level only when no faster level can contain the bug. Risks with no clear right answer, such as "is this screen confusing?", go to a person.

What to leave to people

Automation is worth it when a check will run many times, has a clear (a way to tell right from wrong without a human), and protects a real risk. When one of the three is missing, a person usually does it better and cheaper.

  • No clear oracle: "Is the new slot picker easy to understand?" needs judgement. An exploratory session with a charter (Exploratory testing) finds more here than any script.
  • Runs only once or twice: a one-off data migration check, or a festival banner that lives for a week. Writing and maintaining the test costs more than checking it by hand.
  • Changes every sprint: a screen still being redesigned. Automate its rules now, and the UI check when the design settles.
  • Outside your control: a test that passes only if the payment provider's sandbox is up is an "integrated" test in honeycomb terms. Fake the provider at the API level, and keep one E2E journey against the real sandbox, outside the pull request run if it is unreliable.
Think first: the payment provider's sandbox is down about 2% of the time. Should the wallet journey block every pull request?

Probably not. A 2% outside failure rate would block about one pull request in fifty for reasons nobody on the team can fix, which teaches people to ignore red. Keep the wallet logic in API tests with a fake provider on every pull request. Run the real-sandbox journey on a schedule and before each release, with an owner who reads its failures.

Run the suite like a product

A suite that nobody owns decays like any unowned product. Three habits keep it healthy.

Owners. Every test belongs to a team, written in the folder structure or a code owners file. When a test flakes, its owner gets the ticket, with a deadline.

Code review. Test code gets the same review as product code: clear names, no fixed waits, no shared data between tests, and a reason for its level. The question "could this be one level lower?" saves more minutes than any later speed-up.

A budget in minutes. Decide how long the pull request run may take, then spend it like money. Mangrove's target is 10 minutes. A new E2E journey that adds 40 seconds has to earn them; a new unit test table is almost free. The third diagram shows how the checkout suite spends its budget, stage by stage.

Then measure what tells you whether the suite works:

  • Suite time: how long the pull request run takes, at the median and at the slow end (the 90th percentile).
  • Flake rate: the share of runs where a test failed and then passed on retry with no code change. Playwright, for example, reports such tests as "flaky" when retries are on. Aim for well under 1% of runs, and fix or quarantine the worst tests first. Stable suites in CI covers how.
  • Escaped defects: an is a bug found in production. For each one, ask which level should have caught it, then add that test. The pattern of answers tells you which level is weak.

Two popular numbers say little on their own. The number of tests goes up whenever someone adds a slow, duplicated check. Coverage percentage shows which lines ran, not whether anything was checked (Errors, debugging and testing showed why). Both are fine as a map of blind spots. As targets, they reward the wrong work: teams write tests that execute code and assert nothing.

Quick check

After a quarter of automation, which result best shows the checkout suite is working?

Install 1.5 min Static 1 min Unit 15 s, 38 tests API 45 s, 28 tests Contract 20 s, 4 tests E2E 3 min, 5 tests budget 0 5 min 10 done at 6.8 min
The checkout suite's 10-minute budget, stage by stage. The unit and API stages check most rules in about a minute. The five E2E journeys take almost half the time, which is why each new journey has to earn its place. The rest is headroom for growth.

Build it

Write the test strategy for Mangrove's checkout: map 12 risks to levels, estimate the tests and minutes, make the budget a hard limit in CI, and write the one-page strategy. The next five lessons build each level.

  1. Map the risks to levels

    List what could go wrong, where the bug would live, and the lowest level that can catch it. Keep it in the repository as docs/test-strategy.md, so changes to it get reviewed like code.

    Markdown
    | #  | Risk (what hurts if it breaks)                    | Bug lives in            | Level       | Tests |
    |----|---------------------------------------------------|-------------------------|-------------|-------|
    | 1  | Delivery fee wrong at 2,499 / 2,500 cents         | priceOrder()            | Unit        | 6     |
    | 2  | FRESH10: from 2,000, first order, not with staff  | priceOrder()            | Unit        | 16    |
    | 3  | Staff 5% rounds to the wrong cent                 | priceOrder()            | Unit        | 6     |
    | 4  | Cart 500-50,000 cents, qty 1-20 not enforced      | validateCart()          | Unit        | 10    |
    | 5  | POST /orders accepts a bad body (no 400)          | DTO + ValidationPipe    | API         | 8     |
    | 6  | Order row or lines saved wrong in PostgreSQL      | OrdersService + SQL     | API         | 6     |
    | 7  | Double tap on Pay creates two orders              | idempotency key + index | API         | 3     |
    | 8  | Cancel allowed after picking started              | cancel rules + status   | API         | 6     |
    | 9  | Payment callback does not mark the order paid     | webhook handler         | API (fake)  | 5     |
    | 10 | Renamed field breaks app versions in use          | response shape          | Contract    | 4     |
    | 11 | Sign in, cart, slot, pay (card/wallet/COD), track | app + API + provider    | E2E         | 5     |
    | 12 | Slot picker confusing on small phones             | design                  | Exploratory | 0     |
  2. Estimate the minutes

    Use rough per-test times and the setup each stage needs. Stages run one after another; inside a stage, tests are shared across workers. The API and contract stages use one shared database here, so they run on one worker.

    Output
    Stage      tests  each      workers  setup   wall-clock
    install    -      -         -        90 s    90 s
    static     -      -         -        -       60 s     (tsc + eslint)
    unit       38     ~5 ms     2        15 s    ~15 s    (19 rounds x 5 ms + start)
    api        28     ~150 ms   1        40 s    ~44 s    (Postgres container + app start)
    contract   4      ~1 s      1        16 s    20 s
    e2e        5      ~40 s     2        60 s    180 s    (3 rounds x 40 s + start)
    -------------------------------------------------------------
    total                                        ~409 s = about 6.8 min   budget 10 min: OK
    
    If the 140 recorded UI scripts had stayed:
    e2e        140    ~30 s     4        60 s    35 rounds x 30 s + 60 s = 1,110 s = 18.5 min   over budget

    The numbers are estimates, and that is fine. Their job is to show early that a plan cannot fit, before anyone writes 140 browser tests. The first practice task turns this table into a function.

  3. Make the budget a hard limit

    A budget that only lives in a document drifts. Put it in the tools. In GitHub Actions, timeout-minutes stops a job that runs too long (GitHub Actions: continuous integration covers the rest of the workflow). In Playwright, globalTimeout caps the whole E2E run, and forbidOnly fails CI if someone left a test.only behind.

    TypeScript
    // playwright.config.ts: the E2E stage's share of the budget
    import { defineConfig, devices } from '@playwright/test';
    
    export default defineConfig({
      testDir: './e2e',
      workers: process.env.CI ? 2 : undefined,  // matches the estimate
      retries: process.env.CI ? 1 : 0,          // a pass on retry is reported as flaky, not hidden
      forbidOnly: !!process.env.CI,             // a forgotten test.only fails the run
      globalTimeout: 4 * 60 * 1000,             // milliseconds: the E2E stage gets 4 of the 10 minutes
      reporter: [['html'], ['junit', { outputFile: 'results/e2e-junit.xml' }]],
      projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
    });

    The two Vitest runs assume unit tests named *.spec.ts and a separate config for the API tests; Unit and API tests sets them up, and Stable suites in CI splits this job into parallel jobs with reports.

  4. Write the one-page strategy

    One page, so people read it. Each heading answers a question someone will ask in a review.

    Markdown
    # Checkout test strategy (owner: checkout team, reviewed each quarter)
    
    ## Scope and risks
    Customer app checkout and the order service (POST /orders, GET /orders/:id,
    POST /orders/:id/cancel). Top risks: wrong price, double order, lost payment,
    app and service out of step. Full risk map: table above.
    
    ## Levels and what each owns
    - Unit: every price and cart rule, as test tables from boundary values
    - API: validation, persistence, cancel rules, idempotency; real PostgreSQL,
      payment provider faked
    - Contract: every field the app reads from the order service
    - E2E: 5 journeys (card, wallet, COD, cancel, track); no rule checks here
    - Exploratory: 1 hour per release on new screens, small phones, slow data
    
    ## Budget
    Pull request run under 10 min (today about 7). A new E2E journey needs a
    reviewer's yes and must say which risk no lower level can catch.
    
    ## Data and environments
    Each test creates its own customer and cart with unique ids. E2E runs against
    a preview deployment with seeded data, never against shared staging data.
    
    ## Ownership and flaky tests
    Each folder has an owning team. A flaky test is quarantined within a day,
    with an owner and a fix-by date; quarantine never lasts past two weeks.
    
    ## Metrics (shown on the team dashboard)
    Suite time (median, p90), flake rate (target under 1% of runs), escaped
    checkout defects per month, each tagged with the level that should have caught it.
    
    ## Not automated
    Visual polish, copy, festival banners, one-off migrations.

Bring it to your team

Ask these in planning, in pull request reviews and when a bug escapes.

  • Which risk does this test protect, and could a lower, faster level catch the same bug?
  • How long does our pull request run take today, at the median and the slow end, and what is our budget?
  • What is our flake rate this month, and who owns each test in quarantine?
  • For the last three bugs that reached customers, which level should have caught them?
  • Which E2E journeys do we have, and does each one check wiring instead of repeating rules covered below?
  • What have we decided not to automate, and who checks it by hand before a release?

Red flags

  • Progress reported as "manual cases converted" or "number of tests".
  • A coverage target that rises every quarter while escaped defects stay flat.
  • "It's flaky, just re-run it" with no ticket and no owner.
  • E2E tests that try every discount code through the UI.
  • A suite that runs only at night, so failures arrive the next morning with ten suspects.

Practice

Will the suite fit in ten minutes?

Write planSuite(stages, workers, budgetMinutes = 10). Each stage is { level, count, msEach, setupMs, parallel }. Stages run one after another. Inside a stage, tests of equal length are shared across workers, and each worker runs its share one test after another, so the stage takes setupMs + Math.ceil(count / workers) * msEach. A stage with parallel: false runs on one worker. Return { stages: [{ level, ms }], totalMs, withinBudget }, where withinBudget is true when totalMs is at most the budget (in minutes).

JavaScript exerciseRuns in your browser with tests
function planSuite(stages, workers, budgetMinutes = 10) {
  // Starter: adds up every test as if one machine ran them all, and forgets setup
  const result = stages.map((s) => ({ level: s.level, ms: s.count * s.msEach }));
  const totalMs = result.reduce((sum, s) => sum + s.ms, 0);
  return { stages: result, totalMs, withinBudget: totalMs < budgetMinutes };
}
Ctrl + Enter runs
Show a solution
JavaScript
function planSuite(stages, workers, budgetMinutes = 10) {
  const result = stages.map((s) => {
    const w = s.parallel === false ? 1 : workers;   // shared database: one worker
    const rounds = Math.ceil(s.count / w);          // each worker runs its share in turn
    return { level: s.level, ms: s.setupMs + rounds * s.msEach };
  });
  const totalMs = result.reduce((sum, s) => sum + s.ms, 0);
  return { stages: result, totalMs, withinBudget: totalMs <= budgetMinutes * 60 * 1000 };
}
Pass the exercise to finish this task.

How many flaky tests can you afford?

Write two functions. falseRedRate(flakeRates) takes each test's chance of flaking (0 to 1) and returns the chance that a run is red with no bug, meaning at least one test flakes. maxTestsFor(p, maxRedRate) returns the largest number of tests, each flaking with chance p, that keeps the false-red rate at or below maxRedRate. A test with p of 0 never flakes, so the answer is Infinity.

JavaScript exerciseRuns in your browser with tests
function falseRedRate(flakeRates) {
  // Starter: "1% each, so about 1% overall"
  if (flakeRates.length === 0) return 0;
  return flakeRates.reduce((sum, p) => sum + p, 0) / flakeRates.length;
}

function maxTestsFor(p, maxRedRate) {
  // Starter: assumes the risk grows in a straight line
  return Math.floor(maxRedRate / p);
}
Ctrl + Enter runs
Show a solution
JavaScript
function falseRedRate(flakeRates) {
  // green only if every test passes: multiply the chances of passing
  const allPass = flakeRates.reduce((prod, p) => prod * (1 - p), 1);
  return 1 - allPass;
}

function maxTestsFor(p, maxRedRate) {
  if (p === 0) return Infinity;
  let n = 0;
  while (1 - Math.pow(1 - p, n + 1) <= maxRedRate) n++;
  return n;
}
Pass the exercise to finish this task.

Place three new risks

The checkout team adds three features. For each, name the lowest level that can catch the main risk, and say in one sentence why a lower level cannot. (a) A new rule: cash on delivery is refused for carts over 30,000 cents. (b) The order service starts returning slotLabel instead of slotName, and the app shows the slot on the confirmation screen. (c) A new "repeat last order" button on the home screen that fills the cart through the app and the API.

  • (a) is a unit test of the payment and cart rules, with boundary values at 30,000 and 30,001 cents
  • (b) is a contract test, because app versions already installed still read the old field
  • (c) gets an API test for the new endpoint, and at most one E2E journey for the button's wiring
  • Each answer explains why the level below cannot see the bug
Show a model answer

(a) Unit test. The rule is pure logic on the cart total and the payment method, so table rows for 30,000 and 30,001 cents with each payment method catch every way it can break. Static checks cannot know a business rule. Add one API test only if the rule is enforced in a validation layer outside the pure function.

(b) Contract test. The bug is a disagreement between two things released separately: the service changes on Tuesday, while app versions from months ago stay installed. A unit or API test of the service alone passes, because the service is internally consistent. A contract test from the app's side fails the moment the field disappears.

(c) An API test for the "repeat last order" endpoint (right lines, prices recalculated, out-of-stock items handled), because that is where the logic lives. Then at most one E2E journey that taps the button and sees a filled cart, because only the full stack shows that the button calls the right endpoint. The rules inside stay out of the journey.

Check yourself

A team's E2E suite takes 50 minutes and is red on most nights. A manager proposes buying faster CI machines. What should the team try first?

Two senior engineers argue: one wants a pyramid, the other a honeycomb for the order service. Reading their tests, you see both write the same thing: tests that start the service with a real database and call its endpoints. What is going on?

In one month, three checkout bugs reach customers: a rounding error in the staff discount, a missing 400 for qty: 0, and an old app version crashing on a renamed field. What does this pattern tell you?

Flashcards

Open my review

Key takeaways

  • Suites fail teams by being slow, flaky and aimed at the wrong things, until nobody trusts red. Flakiness multiplies with the number of tests.
  • Each level trades speed, isolation and upkeep for confidence. The pyramid, trophy and honeycomb all keep E2E few.
  • Start from risks, push each check to the fastest level that can catch it, and leave judgement calls to people.
  • Run the suite like a product: owners, code review and a budget in minutes, enforced in CI.
  • Watch suite time, flake rate and escaped defects; treat test count and coverage as maps of blind spots.

Sources

Episode transcriptTasting the sauce beats mystery diners · about 12 minutes · tap any line to play from there
  1. NadiaImran, picture this. The checkout team has an automated suite. It's red almost every night. Nobody reads the failures any more. They press re-run, and if it's still red, they merge anyway.
  2. ImranThat sounds worse than having no tests at all.
  3. NadiaIn a way it is. It costs money to run, and it protects nothing. Today is about how a team ends up there, and how to plan a suite that people actually trust.
  4. NadiaStart with a kitchen. A good cook tastes the sauce at the stove, many times a night. Two seconds each time, and when it's wrong, the cook knows exactly which pot to fix.
  5. ImranAnd then the head chef checks the plates before they go out.
  6. NadiaRight. That's slower, and it catches things no single cook sees. The sauce on the wrong dish, a cold side. And once a month the owner sends a mystery diner, who books a table, eats a full meal and writes a report.
  7. ImranWhich tells you the whole experience was bad, but not which station caused it.
  8. NadiaExactly. Nobody sensible replaces tasting with a hundred mystery diners. But that's what many automation projects do. They take every manual test case and turn it into a script that clicks through the app.
  9. ImranSo the tasting is unit tests, the plate check is integration or API tests, and the mystery diner is the end-to-end test.
  10. NadiaThat's the picture. Now, why do suites fail teams? Four reasons. They're slow, so they don't run on every pull request. They're flaky, meaning they go red with no bug. They test the wrong things. And in the end, nobody trusts red.
  11. ImranFlaky I understand. But surely a test that flakes one time in a hundred is fine?
  12. NadiaOne of them is fine. That's the trap. A run is green only when every single test passes. So the chances multiply. Fifty tests that each flake one percent of the time give you a red run about forty percent of the time.
  13. ImranForty percent? I'd have guessed one percent overall.
  14. NadiaMost people guess that. With two hundred such tests, it's about eighty-seven percent of runs. No amount of discipline beats that arithmetic. You fix it by having fewer tests at the level that flakes.
  15. ImranWhich is the end-to-end level, I assume.
  16. NadiaMostly, yes. Let's go through the levels with our checkout. Static checks first. TypeScript and a linter read the code without running it. Seconds, and they never flake.
  17. NadiaUnit tests call one function, like the price calculation, with no network and no database. About a millisecond each, and a failure names the exact rule.
  18. ImranAnd API tests start the order service for real?
  19. NadiaWith a real Postgres database, and they call the endpoint over HTTP. Roughly fifty to two hundred milliseconds each. They catch validation, SQL and the JSON the service sends back. Contract tests check that the app and the service still agree on field names.
  20. NadiaAnd end-to-end tests drive the real app with everything behind it. Twenty to sixty seconds each. They're the only level that sees the button, the API and the payment page working together.
  21. ImranSo going up a level buys confidence, and you pay in speed.
  22. NadiaIn speed, in isolation, and in upkeep. When an end-to-end test fails, it could be the app, the service, the test data or the network. You spend twenty minutes finding out which.
  23. NadiaIf you can see your screen, the first diagram shows three shapes side by side. If not, picture a pyramid, a trophy and a honeycomb, each made of stacked bands. A wider band means more tests at that level.
  24. ImranThe pyramid I know. Lots of unit tests at the bottom, a few UI tests on top.
  25. NadiaThat's Mike Cohn's, from his two thousand nine book. Its argument is cost. Kent C. Dodds drew the trophy in two thousand eighteen. Static checks at the base, and the widest band is integration tests, because they use your code the way users do.
  26. ImranAnd the honeycomb?
  27. NadiaFrom Spotify's engineering blog, also two thousand eighteen, for microservices. Mostly integration tests of one service through its API with a real database. A few tests for code with tricky logic. And ideally no tests that pass or fail depending on whether another service is correct.
  28. ImranSo which one is right? They seem to disagree.
  29. NadiaLess than it looks. Martin Fowler pointed out that much of the fight is about what the word unit means. And all three agree on two things. End-to-end tests stay few. And fast tests inside one service or app do most of the work.
  30. ImranSo you don't choose a shape for the company.
  31. NadiaYou choose a level for each risk, and the shape appears by itself. Our price rules are pure logic with lots of combinations, so that part looks like a pyramid. The order service is mostly validation and SQL, so it looks like a honeycomb.
  32. ImranGive me the rule for choosing, then.
  33. NadiaPush each check down to the fastest level that can catch the bug. Take FRESH10. Ten percent off from twenty dollars, first order only, never with the staff discount. Three conditions, lots of combinations.
  34. ImranAnd all of that lives in the price function. So a table of unit tests, a few milliseconds for the lot.
  35. NadiaYes. Through the UI, each combination would cost a minute. Now, a customer double taps Pay. Two identical requests must create one order. Can a unit test prove that?
  36. ImranNo. You need the real database and its unique key. So that's an API test.
  37. NadiaRight. And if the service renames the delivery fee field, the app versions already on people's phones break. That's two things released separately, so it's a contract test. Only paying by wallet and seeing the confirmation needs the full end-to-end journey.
  38. NadiaThe second diagram turns this into questions you ask from the cheapest level up. Is there a clear right answer? Is the bug in pure logic? Does it stay inside one service? Do two apps released apart have to agree? You stop at the first yes.
  39. ImranAnd if there's no clear right answer?
  40. NadiaThen a person should check it. Is the new slot picker confusing? That's an exploratory session, not a script. Same for things that run once, or screens the designer changes every sprint.
  41. ImranThis is what happened with our checkout, isn't it? Two hundred manual cases, two testers, two days per release.
  42. NadiaYes. The first attempt recorded a hundred and forty of them as UI scripts. Thirty seconds each, seventy minutes on one machine, so it ran only at night. With roughly one percent flakiness each, about three nights in four were red with no bug.
  43. ImranAnd within two months people merged on red.
  44. NadiaSo we started again from risks. A hundred and twenty cases were price and discount rules. They became about forty unit test rows, under a second. Forty-five were order service behaviour, which became about thirty API tests. Ten became four contract tests.
  45. ImranAnd the end-to-end ones?
  46. NadiaSix journeys, two of them the same, so five. The last nineteen were look and feel, and they became a one-hour exploratory session before each release. The whole suite now runs in about seven minutes on every pull request.
  47. ImranSo seven minutes. Is there a target?
  48. NadiaTen minutes. That's the third idea. Treat the suite like a product. It has owners, its code gets reviewed like product code, and it has a budget in minutes that you spend like money.
  49. NadiaThe third diagram shows that budget as bars along a ten-minute line. Install takes a minute and a half, static checks a minute. Unit, API and contract tests together take under a minute and a half. The five journeys take three minutes. Done at about six point eight minutes.
  50. ImranSo the five journeys cost more than all the other tests combined.
  51. NadiaWhich is why a new journey has to earn its forty seconds. A new unit test table is almost free.
  52. ImranHow do you know whether the suite is working? My instinct is to report the number of tests.
  53. NadiaThat's the reasonable guess, and it's a weak one. The test count goes up whenever someone adds a slow duplicate. Coverage percent shows which lines ran, and says nothing about whether anything was checked.
  54. ImranSo what do you watch instead?
  55. NadiaThree things. Suite time, at the median and the slow end. Flake rate, the share of runs where a test failed and then passed on retry with no change. And escaped defects, bugs that reached customers, each tagged with the level that should have caught it.
  56. NadiaIn the Build it section you write the strategy. First, a table of twelve checkout risks, where each bug would live, and its level. Then you estimate the tests and minutes for each stage. Then you put the budget into the tools, with a job timeout in CI and a global timeout in the Playwright config.
  57. ImranAnd the document itself?
  58. NadiaOne page. Scope and risks, what each level owns, the budget, test data, owners and the flaky test rule, the metrics, and what you don't automate. One page, so people actually read it.
  59. ImranFor my next review, then. Which risk does this test protect, and could a lower level catch it? How long does our pull request run take, and what's the budget? And for the last three bugs that reached customers, which level should have caught them?
  60. NadiaThose three questions would change most teams. Now a quick one for you. The payment provider's test sandbox is down about two percent of the time. Should the wallet journey against it block every pull request? Take a second.
  61. ImranI'd say no. It would block one pull request in fifty for something we can't fix.
  62. NadiaRight. Test the wallet logic at the API level with a fake provider on every pull request. Run the real sandbox journey on a schedule and before releases, with an owner who reads its failures.
  63. ImranLet me sum up. One, flaky tests multiply, so a big end-to-end suite goes red all the time. Two, push every check to the fastest level that can catch it. Three, a suite needs owners, a minute budget, and the right metrics.
  64. NadiaGood. One nuance on the second point. Keep a few end-to-end journeys. They're the only check that the whole thing is wired together. Few is the goal, and zero would be a mistake.
  65. ImranNext time?
  66. NadiaThe bottom of the pile: unit and API tests for the price rules and the order service, with test data that doesn't leak between tests.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Test plan

Scope, risks, environments and entry/exit criteria as data, with checks that every risk has tests.

Open in the QA Lab
Next stopUnit and API tests
Was this lesson helpful?
Questions and discussion

Loading the discussion…