Testing Pyramid & E2E Strategy
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ธ๏ธ Tests are safety nets โ the pyramid shape tells you how dense each layer should be.
The safety net analogy
Unit tests = fine mesh net at the bottom. Catches small bugs (logic errors in individual functions) instantly. Very cheap โ thousands of them are fine.
Integration tests = medium mesh net in the middle. Catches bugs where components interact incorrectly. Moderate cost โ hundreds is reasonable.
E2E tests= coarse mesh net at the top. Only catches things that slip through both layers โ full user journey failures. Very expensive โ tens is the limit. Don't try to catch everything here.
The Ice Cream Cone (anti-pattern)
No unit tests. A few integration tests. Hundreds of E2E tests. CI takes 3 hours. Tests are flaky โ 10% fail on any given run for no clear reason. Developers ignore failures. The suite exists but provides no confidence. This is the most common testing anti-pattern in real codebases.
Interactive Sandbox
โ Move something, see it react instantly.Testing Layer
Unit Tests
70%+ of testsTest a pure function: formatCurrency(1234.5, 'USD') === '$1,234.50'
Good for
Pure functions, business logic, edge cases
Bad for
Integration bugs, UI rendering, real API behavior
Integration Tests
20% of testsRender <LoginForm />, fill email/password, click submit, assert API called and redirect occurs
Good for
Component behavior, form flows, API integration
Bad for
Full user journeys, browser-specific bugs
E2E Tests
10% of tests (critical paths only)Navigate to /signup, fill form, verify email, log in โ real browser, real HTTP
Good for
Critical user journeys, cross-browser, auth flows
Bad for
Edge cases, rapid iteration, every code path
Challenge
Visit all 5 testing patterns. Understand when to use each test layer and how to write effective tests.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What is the test pyramid and why does it matter?
The test pyramid describes the ideal distribution: many fast, cheap unit tests at the base; fewer, slower integration tests in the middle; very few expensive E2E tests at the top. It matters because speed = developer feedback loops. A test suite that runs in 30 seconds gets run constantly. A suite that runs in 30 minutes gets skipped. The pyramid keeps the suite fast by keeping expensive tests rare.
Q: What is the difference between mock, stub, and spy?
Stub: a replacement with hard-coded return values โ replaces real implementation, returns predictable data. Mock: a stub that also verifies it was called correctly โ asserts that specific calls happened with expected arguments. Spy: wraps a real implementation and records calls without replacing behavior โ useful to verify a real function was called. In Jest/Vitest: jest.fn() creates a mock, jest.spyOn() creates a spy.
Q: How does RTL's philosophy differ from Enzyme?
Enzyme tests the React component tree โ you access component state, call lifecycle methods directly, and inspect internal structure. RTL tests the rendered DOM and user interactions โ you query by ARIA role and label, simulate user events, and assert on what the user sees. Enzyme broke constantly during React upgrades because it depended on React internals. RTL's DOM-based approach is decoupled from React implementation details.
The Deep Dive
โ Spec refs, engine internals, the minutiae.MSW setup for tests
| 1 | // src/mocks/handlers.ts |
| 2 | import { http, HttpResponse } from 'msw'; |
| 3 | ย |
| 4 | export const handlers = [ |
| 5 | http.get('/api/user', () => |
| 6 | HttpResponse.json({ id: '1', name: 'Alice' }) |
| 7 | ), |
| 8 | http.post('/api/login', async ({ request }) => { |
| 9 | const { email } = await request.json(); |
| 10 | if (email === 'valid@test.com') { |
| 11 | return HttpResponse.json({ token: 'abc123' }); |
| 12 | } |
| 13 | return HttpResponse.json({ error: 'Invalid' }, { status: 401 }); |
| 14 | }), |
| 15 | ]; |
| 16 | ย |
| 17 | // src/mocks/server.ts |
| 18 | import { setupServer } from 'msw/node'; |
| 19 | import { handlers } from './handlers'; |
| 20 | export const server = setupServer(...handlers); |
| 21 | ย |
| 22 | // vitest.setup.ts |
| 23 | import { server } from './src/mocks/server'; |
| 24 | beforeAll(() => server.listen()); |
| 25 | afterEach(() => server.resetHandlers()); |
| 26 | afterAll(() => server.close()); |
Coverage metrics explained
| Metric | What it measures | Limitation |
|---|---|---|
| Statement | Every line executed | Ignores branches |
| Branch | Every if/else path taken | Best proxy for logic coverage |
| Function | Every function called | Easy to game โ empty test calls function |
| Line | Every line hit (excludes blanks) | Similar to statement |
Branch coverage is the most meaningful metric โ it ensures both sides of every conditional are tested. 80% branch coverage is a reasonable gate. 100% is usually not worth the effort (test every null check?).
Vitest vs Jest
Jest
- Industry standard, massive ecosystem
- Own module resolution (sometimes surprising)
- Slower on large codebases
- CJS-first, ESM support improving
Vitest
- Vite-native, shares vite.config.ts
- 10โ20x faster with HMR test rerun
- ESM-first, TypeScript natively
- Jest-compatible API โ easy migration
Interview Questions
โ Real questions from real interviews โ with answers.Over-relying on E2E tests made our CI suite too slow and flaky to trust.
Measure the bug rate in untested vs tested files and present the correlation.
Unit test pure logic; integration test user-facing behavior; skip trivial plumbing.
MSW intercepts at the network layer โ your real fetch code runs, the response is controlled.
Quarantine immediately, fix root cause within one sprint, delete if unrepairable.
Branch coverage over line coverage; 80% is a reasonable gate but quality beats quantity.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is a 'snapshot test' and when should it be avoided?