RMRM Full Stack & AI Engineer · All guides · Roadmaps
Engineering · guide

Unit vs Integration vs E2E Tests

A practical breakdown of the three core layers of software testing — unit, integration, and end-to-end — explaining what each tests, why each matters, and how to use them together effectively.

What Are the Three Layers?

Unit tests verify the smallest pieces of code in isolation — a single function, method, or class — with all external dependencies mocked or stubbed out. Integration tests check that two or more modules or services work correctly together, such as a controller calling a real database or an API consuming a third-party service. End-to-end (E2E) tests simulate a real user journey through the fully assembled application, from the UI down to the database. Together these three layers form the classic 'Testing Pyramid', which guides how many tests of each type to write.

Unit Tests: Speed and Precision

Unit tests run in milliseconds because they avoid the filesystem, network, and database entirely, relying on fakes and mocks instead. They pinpoint exactly which function broke when a test fails, making debugging fast and deterministic. A well-written unit test has a single assertion target: arrange inputs, act on the unit under test, and assert the output. Most codebases should have the highest number of unit tests relative to the other layers.

Integration Tests: Real Boundaries

Integration tests exercise the contracts between components — for example, confirming that a repository class correctly maps SQL rows to domain objects using a real test database. They catch bugs that unit tests with mocks can never find, such as mismatched SQL column names or incorrect HTTP response parsing. Integration tests are slower than unit tests because they spin up real dependencies, often managed via Docker or an in-memory database. Aim for a moderate number: enough to cover critical integration points without duplicating unit test coverage.

E2E Tests: Full User Journeys

E2E tests drive a real browser or API client through complete user scenarios — login, checkout, form submission — against a deployed or locally running application stack. Tools like Playwright, Cypress, and Selenium automate browser interactions, while Supertest or Postman/Newman cover API-level E2E flows. Because every layer of the system is involved, E2E tests are the slowest and most brittle of the three, susceptible to flakiness from timing issues or environment differences. Keep the E2E suite small and focused on the highest-value, highest-risk user paths only.

The Testing Pyramid and Proportions

The Testing Pyramid, popularized by Mike Cohn, recommends a large base of unit tests, a medium layer of integration tests, and a small peak of E2E tests. This ratio optimizes for fast feedback loops: developers get sub-second results from unit tests on every save, integration tests run in CI on each push, and E2E tests run before deployment. Inverting the pyramid — writing mostly E2E tests — leads to slow, flaky pipelines that erode developer confidence. A common healthy ratio is roughly 70% unit, 20% integration, and 10% E2E by test count.

Key Gotchas and Best Practices

The biggest pitfall is over-mocking in unit tests: if you mock every dependency, tests pass even when integrations are completely broken, giving false confidence. Conversely, writing integration or E2E tests for simple pure-function logic is wasteful and slow. Use test doubles (mocks, stubs, spies) only at true architectural boundaries — external APIs, databases, message queues — not between internal modules you own. Always run the full test suite in CI/CD and treat a failing test as a build-breaking event, never something to skip or ignore.

Go deeper with an AI tutor that teaches this in context — and quizzes you on it.
Open the app — free to start

© RM Full Stack & AI Engineer · All guides · Roadmaps · Open the app