Learning Material

Testing Strategies: Unit, Integration, and End-to-End

Learning Material · Software Engineering

Cover: Testing Strategies: Unit, Integration, and End-to-End

The testing pyramid

       /\
      /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.

Unit tests: one function, in isolation

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.

Integration tests: pieces working together

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()
})

End-to-end tests: the whole user journey

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.

Flaky tests are worse than no tests. A test that fails intermittently for reasons unrelated to real bugs trains a team to ignore failures — including real ones. Fixing or deleting a flaky test is almost always worth prioritizing over adding new tests on top of an unreliable foundation.

What "good coverage" actually means

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