Testing Strategy
Reference for backend test selection and examples by risk area. Read when writing tests, reviewing test coverage, or establishing test patterns.
Table of Contents
- Unit Tests
- Integration Tests
- API Tests
- Repository and Model Tests
- Migration Tests
- Worker Tests
- Webhook Tests
- Auth Negative Tests
- Test Data Factories
- External Service Mocks
Unit Tests
Test pure business logic in isolation. No database, no network.
When: pricing calculations, validation rules, domain state machines, permission checks, data transformations, utility functions.
// Pseudocode
test "apply 10% discount to order over $100":
order = Order(items: [Item(price: 120)])
discounted = applyDiscount(order, percent: 10)
assert discounted.total == 108Integration Tests
Test components working together with real dependencies (database, cache).
When: repository queries, ORM behavior, transaction boundaries, cache read/write, queue publish/consume.
- Use test database with migrations applied.
- Clean up data between tests (transaction rollback or truncate).
API Tests
Test full request → response cycle through the application.
When: route behavior, middleware chains, request validation, response shape, status codes, auth enforcement.
// Pseudocode
test "POST /orders returns 201 with valid data":
response = request.post("/orders", body: validOrder, auth: userToken)
assert response.status == 201
assert response.body.data.id exists
test "POST /orders returns 400 with missing fields":
response = request.post("/orders", body: {}, auth: userToken)
assert response.status == 400
assert response.body.error.code == "VALIDATION_ERROR"Repository and Model Tests
Test data access layer behavior.
When: complex queries, custom scopes/filters, soft delete behavior, computed fields, relationship loading.
- Test against real database.
- Verify correct SQL generation for edge cases.
Migration Tests
Test schema changes don't break existing data.
When: column type changes, data backfills, constraint additions, index modifications.
- Run migration up, verify schema state.
- Run migration down, verify rollback.
- Test with realistic data volumes when migration performance matters.
Worker Tests
Test background job behavior.
When: async processing, queue consumers, scheduled tasks, retry behavior.
- Test idempotency: process same message twice, verify no duplicate side effects.
- Test failure handling: simulate errors, verify retry/DLQ behavior.
- Test timeout: verify job doesn't run forever.
Webhook Tests
Test webhook receipt and processing.
When: incoming webhooks from external services (Stripe, GitHub, Slack).
- Test signature verification (valid, invalid, missing).
- Test replay protection (old timestamps rejected).
- Test idempotent processing (duplicate webhook IDs handled).
- Test async processing (webhook returns 200 quickly, processes in background).
Auth Negative Tests
Test security boundaries. These are the most commonly missed tests.
Must test:
- Unauthenticated request → 401
- Wrong role/permission → 403
- Access other user's resource → 403 or 404
- Access other tenant's resource → 403 or 404
- Expired token → 401
- Invalid token → 401
- Malformed auth header → 401
// Pseudocode
test "user cannot access another tenant's orders":
order = createOrder(tenantId: "tenant_A")
response = request.get("/orders/" + order.id, auth: tenantBUserToken)
assert response.status == 404 // not 403, to prevent enumerationTest Data Factories
Build reusable test data builders for consistent test setup.
// Pseudocode
function createUser(overrides = {}):
defaults = { name: "Test User", email: unique_email(), role: "member", tenantId: "test_tenant" }
return db.create("users", { ...defaults, ...overrides })
function createOrder(overrides = {}):
user = overrides.user or createUser()
defaults = { userId: user.id, status: "pending", total: 100 }
return db.create("orders", { ...defaults, ...overrides })- Generate unique values for constrained fields (email, slug).
- Support overrides for specific test scenarios.
- Handle relationships (create dependent records automatically).
External Service Mocks
Mock external services at the HTTP boundary, not at the client class level.
- Use HTTP interception libraries (nock, responses, httptest, WireMock).
- Record real responses for realistic mocks.
- Test both success and failure scenarios (timeout, 500, rate limited, malformed response).
- Verify outgoing request shape (URL, headers, body).
- Never call real external services in automated tests.