Checklist and Capstone
The checklist
Unit tests
- Transformers tested (correct output shape, null handling, edge values)
- Zod schemas tested (valid input passes, invalid input fails, edge cases)
- Helper functions tested (pure functions with known input/output)
- Business logic tested with test database
Integration tests
- Every GET endpoint: happy path, empty results, with query params, 404
- Every POST endpoint: creation, 201 + Location, persistence, validation errors
- Every PUT/PATCH endpoint: update, partial update, not found, validation
- Every DELETE endpoint: deletion, 204, not found
- Error responses: consistent shape, correct status codes, field-level errors
Authentication tests
- Protected endpoints return 401 without token
- Protected endpoints return 401 with invalid/expired token
- Admin endpoints return 403 for non-admin users
- Protected endpoints succeed with valid token
Test infrastructure
- Test database (in-memory SQLite)
- Database isolation (clean state per test)
- Factory functions for test data
- Request helpers (get, post, put, del)
- Auth helpers (createTestToken, authHeader)
- Assertion helpers (expectJson, expectError)
Advanced
- External services mocked (email, payment)
- Background job enqueueing verified
- Job handlers tested directly
- Edge cases: empty strings, boundaries, special characters, type confusion
- Tests run in CI on every push
Common mistakes
Testing the framework. Writing tests for Response.json() or route matching. Trust the framework — test your code.
Shared state between tests. Test A creates data that Test B depends on. Use beforeEach to reset state. Each test must be independent.
Only testing happy paths. The API works with perfect input. What about empty strings? Missing fields? Invalid types? Unauthorized users? Error paths have more bugs than happy paths.
Mocking too much. If every dependency is mocked, you are testing mock behavior, not real behavior. Mock external services. Do not mock the database in integration tests — use a real test database.
Mocking too little. Tests that send real emails or charge real credit cards. Always mock external services with real-world side effects.
No test for the bug. A user reports a bug. You fix it. But without a test, the same bug can return in a future change. Every bug fix should include a test.
Brittle tests. Testing exact error messages: expect(body.error.message).toBe("String must contain at least 1 character(s)"). Zod might change the message. Test the structure: expect(body.error.fields.title).toBeDefined().
The fully tested book catalog
Test Files 8 passed (8)
Tests 47 passed (47)
tests/unit/transformers.test.ts
✓ formatBookV1 returns flat author string
✓ formatBookV2 nests author as object
✓ formatBookV2 handles null rating
✓ formatBookV2 converts to camelCase
tests/unit/schemas.test.ts
✓ CreateBookV2 accepts valid input
✓ CreateBookV2 rejects missing title
✓ CreateBookV2 rejects invalid genre
✓ CreateBookV2 trims whitespace
✓ BookQueryV2 coerces string to number
✓ BookQueryV2 applies defaults
tests/integration/books.test.ts
✓ GET /v2/books returns all books
✓ GET /v2/books returns empty for no books
✓ GET /v2/books filters by genre
✓ GET /v2/books/:id returns book with ratings
✓ GET /v2/books/:id returns 404 for nonexistent
✓ POST /v2/books creates and returns 201
✓ POST /v2/books persists in database
✓ POST /v2/books returns Location header
✓ POST /v2/books rejects missing title (400)
✓ POST /v2/books rejects invalid genre (400)
✓ DELETE /v2/books/:id returns 204
tests/integration/auth.test.ts
✓ returns 401 without token
✓ returns 401 with expired token
✓ returns 403 for non-admin delete
✓ allows admin delete
tests/integration/errors.test.ts
✓ 400 error has consistent shape
✓ 404 error has consistent shape
✓ all errors are JSON
tests/advanced/edge-cases.test.ts
✓ handles unicode title
✓ rejects whitespace-only title
✓ rejects title exceeding max length
✓ boundary: rating 1 accepted
✓ boundary: rating 5 accepted
✓ boundary: rating 0 rejected
✓ boundary: rating 6 rejected Every endpoint, every error case, every edge case. Run npm test and know the API works.
Challenges
Challenge 1: Add test coverage reporting. Run vitest --coverage. Identify untested lines. Add tests to reach 90% coverage.
Challenge 2: Add performance tests. Measure response time for each endpoint. Assert it is below a threshold (e.g., 50ms). Catch accidental N+1 queries.
Challenge 3: Add contract tests. Verify v1 and v2 responses match their documented contracts. If the contract changes, the test fails.
Challenge 4: Test the caching layer. Verify cache hits return the same data as cache misses. Verify invalidation clears the correct entries.
What is the most important testing principle for APIs?