Backend testing layers, with examples β
What gets tested at each layer when the system under test is a backend. To make it concrete, a running example: the projects REST API of a threat modeling platform β CRUD for projects and their components authenticated with JWT, editor/viewer roles, input validation, and the business rule "duplicate components are not allowed within the same project".
Unit: the logic, in isolation β
Each function/method is tested with all dependencies mocked (repository, external services):
PermissionService.canEdit()returnsfalsefor a user with the viewer role.ProjectService.addComponent()throws an error if the component already exists in the project (mocked repository).JWTUtils.decodeToken()parses the token correctly.
It's the fastest and cheapest layer to maintain. Tools: JUnit, pytest, Jest/Vitestβ¦
Integration: real components interacting β
Here there's already a real database (or a realistic one: in-memory H2/SQLite, or containers with Testcontainers):
POST /projectscreates the project and persists it in the DB.GET /projects/{id}returns what's in the DB.PUT /projects/{id}updates and records the change in the audit service.
A tip I apply: include OpenAPI contract validation at this layer β it's cheap contract testing within your own team.
API E2E: a consumer's complete flow β
The whole stack up and running (auth, DB, services), and the test walks through a real business flow:
POST /auth/loginβ get the JWT of a user with the editor role.- With the token,
POST /projectswith a valid payload. - Validate the response and confirm persistence with
GET /projects/{id}. PUT /projects/{id}adding a component and validate the new data (with a viewer token, check it returns403).DELETE /projects/{id}and check that it no longer exists.
Operational summary β
| Layer | Goal | Typical tools | When it runs | Blocking? |
|---|---|---|---|---|
| Unit | Isolated functions, no DB or network | JUnit, pytest, Jest | Every commit/push | Yes, always |
| Integration | API β DB, services β queues, OpenAPI contract | Spring Test, Supertest, Testcontainers | Every pull request | Yes (contracts and DB) |
| API E2E | Business flows with the full stack | REST Assured, Postman/Newman, Karate | Main/staging and pre-release | Yes for release |
Two final tips I only remember too late when I fail to apply them:
- Measure unit and integration coverage separately. Mixing them produces an aggregate figure that gives false confidence.
- Tag the tests (
@unit,@integration,@e2e) so each pipeline runs exactly its own subset.