Skip to content

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() returns false for 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 /projects creates 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:

  1. POST /auth/login β†’ get the JWT of a user with the editor role.
  2. With the token, POST /projects with a valid payload.
  3. Validate the response and confirm persistence with GET /projects/{id}.
  4. PUT /projects/{id} adding a component and validate the new data (with a viewer token, check it returns 403).
  5. DELETE /projects/{id} and check that it no longer exists.

Operational summary ​

LayerGoalTypical toolsWhen it runsBlocking?
UnitIsolated functions, no DB or networkJUnit, pytest, JestEvery commit/pushYes, always
IntegrationAPI ↔ DB, services ↔ queues, OpenAPI contractSpring Test, Supertest, TestcontainersEvery pull requestYes (contracts and DB)
API E2EBusiness flows with the full stackREST Assured, Postman/Newman, KarateMain/staging and pre-releaseYes 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.