lecture 7 of 160 completed

A policy for stubbing, not a habit

Stubbing makes tests fast and stable. Done without a policy it produces a green suite over a broken application.

A team I worked with stubbed almost every request. The suite was fast and steady for eight months. Then a backend release renamed a field, every screen showed blank names in production, and all two hundred tests were green while support handled the complaints.

Nobody was careless. Each individual decision to stub was reasonable. What was missing was a policy, and writing that policy is senior work.

The trade, stated plainly. Stubbing replaces the server's answer with one you control. You gain speed, determinism, and access to states you cannot easily create. You lose the only thing that was checking whether your app and your server still agree.

That last clause is the whole risk. A stubbed test cannot detect API drift, by definition.

A policy that works. Three tiers, and every test belongs to exactly one.

Tier one: fully real. No stubbing at all.

The critical journeys. Login, checkout, the main create-read-update-delete flow of your product. These go all the way to a real server and a real database.

Keep this set small, maybe fifteen to thirty tests. They are your contract check: if the backend changes shape, these break, and that is exactly what you want them to do.

Tier two: real by default, stub the awkward parts.

Most of your suite. The app talks to the real server, but you stub the third-party payment provider, the flaky external address lookup, the email service. Anything you do not own and cannot control.

This is where the majority of tests should live.

Tier three: fully stubbed.

The states you cannot otherwise reach: a 500 error, an empty list, a list of five thousand items, a slow response that should show a spinner, a permissions shape your test data does not have.

These tests are checking your UI's reaction, not the integration. Stubbing is not a compromise here, it is the correct tool.

⚠️ The failure mode is drift, and it is silent.

A stub is a snapshot of what the API returned on the day you wrote it. The API moves. The stub does not. Nothing fails, because the stub is the thing being tested.

Two defences. Keep tier one fully real, so drift breaks something. And where the stakes are high, generate stubs from the API's own schema rather than hand-writing them, so a schema change invalidates them.

Making the tier visible. The policy only works if you can see which tier a test is in. Tag them:

describe('checkout [integration]', () => { /* tier one, no stubs */ });
describe('checkout errors [stubbed]', () => { /* tier three */ });

Now you can run tiers separately, and a reviewer can see at a glance whether a new test stubs something it should not.

A rule for reviews. When a pull request adds a stub to a tier-one test, that is worth a conversation. Usually it means someone hit a flaky integration and reached for the quickest fix, which converts a genuine warning into silence.

Waiting on requests is different from stubbing. cy.intercept without a response body just watches. That is not stubbing and carries none of this risk. Use it freely, because waiting on a real request is far better than waiting on a duration.

Remember this: decide the tier per test and write it down. Keep a real tier that would break on drift, because a suite that cannot break is not protecting you.

Reference: cy.intercept.