/\
/E2E\ few, slow, high confidence
/------\
/Integr. \ some, moderate speed
/----------\
/ Unit Tests \ many, fast, narrow scope
/--------------\
The pyramid shape is deliberate: many fast, narrow unit tests at the base, fewer broader integration tests in the middle, and a small number of full end-to-end tests at the top. Inverting this shape — few unit tests, heavy reliance on slow end-to-end tests — is a common, expensive mistake that makes a test suite slow and brittle.
function calculateDiscount(price, percent) {
return price - (price * percent / 100)
}
test('applies a 10% discount', () => {
expect(calculateDiscount(100, 10)).toBe(90)
})
A unit test checks one small piece of logic, with everything around it (databases, network calls, other modules) mocked out or simply absent. They run in milliseconds, which is exactly why you want hundreds or thousands of them — fast feedback on logic errors, run on every single save.
An integration test checks that multiple units — a route handler and a real (or realistic test) database, for instance — work correctly together. This catches a category of bugs unit tests structurally cannot: a correct function called with the wrong arguments by another correct function, or a database query that's syntactically valid but returns the wrong data.
test('POST /users creates a user in the database', async () => {
const res = await request(app).post('/users').send({ name: 'Amara' })
expect(res.status).toBe(201)
const saved = await db.users.findOne({ name: 'Amara' })
expect(saved).not.toBeNull()
})
An E2E test drives a real (or realistic) browser through an actual user flow — logging in, adding an item to a cart, checking out — verifying the entire system works together, frontend included. They're the slowest and most brittle layer (a CSS change can break a test that has nothing to do with the logic being tested), which is exactly why the pyramid keeps them few and reserved for the most critical user flows.
A high test coverage percentage measures how much code executed during tests, not whether the important logic and edge cases are actually verified. 100% coverage with weak assertions (or none) is close to worthless; 70% coverage that thoroughly tests the critical paths and edge cases is worth far more. Coverage is a useful signal for finding untested code, not a target to maximize for its own sake.
Comments