Unit tests check one piece of logic, integration tests check your code with a real dependency, and end-to-end tests check the whole running system from outside. Balance them by risk.
A unit test checks one piece of logic, such as a service method or a pure function. Its collaborators (the other classes it calls) are replaced by test doubles: fake versions that you control. A unit test runs in milliseconds. When it fails, it points straight at the problem.
An integration test checks your code together with a real dependency. Examples:
- a repository against a real Postgres database
- a cache layer against a real Redis
- a controller called through the HTTP layer, with a real database behind it
Integration tests catch bugs that mocks hide: SQL bugs, mapping bugs, transaction bugs and configuration bugs.
An end-to-end (e2e) test drives the running system from the outside, over HTTP and across services, the same way a client would.
Balance them by risk:
- Write unit tests for real business rules.
- Write a solid layer of integration tests for every boundary you own: the database, queues and HTTP.
- Write a few e2e tests for critical user journeys, like signup and checkout. Keep them few, because they are slow and the most likely to be flaky (pass or fail at random).
- A unit test checks one piece of logic, with its collaborators replaced, and runs very fast.
- An integration test runs your code with a real dependency, such as Postgres or Redis.
- An end-to-end test drives the running system from outside, the way a client would.
- Balance the tests by risk and cost, and keep only a few e2e tests for critical journeys.