Testing Strategy
This project uses a three-layer testing approach to ensure API correctness and reliability.
Testing Layers
- Route Tests (Vitest) – Contract compliance, error handling.
- Unit Tests (Vitest) – Helper and service logic.
- E2E Tests (Playwright) – Full HTTP requests with real database.
Quick Start
pnpm test # Run all unit tests (routes, helpers, services)
pnpm run integration # Run E2E tests
Route Tests
Route tests verify that API handlers return correct responses and handle errors properly. They run fast because external dependencies (database, APIs) are mocked.
File Organization
Tests are co-located with their routes:
app/routes/admin/options/
├── index.ts # Route handler
└── index.test.ts # Tests for this route
What We Test
For each route, we typically test:
- Happy path – Returns correct data matching the OpenAPI schema
- Validation errors – E.g. return 400 for invalid input
- Server errors – E.g. returns 500 when dependencies fail
What We Mock
- Database queries (
vi.mock("../database/queries/*.server.js")) - External services (Unleash, Statista API, etc.)
Test Helpers
Shared utilities in tests/helpers/ reduce boilerplate:
tests/helpers/
├── builders.ts # e.g. buildOption(), buildStep()
├── response-schemas.ts # e.g. OptionSchemaWithStringDates for API validation
├── request-factories.ts # e.g. createOptionsRequest()
└── get-mock-db.ts # Mock database setup
Writing Good Tests
Each test should catch a unique bug. Ask: "What does this test catch that no other test catches?"
✅ Good test suite structure:
"returns options matching OpenAPI schema"– Happy path + contract"returns 400 for invalid request body"– Input validation"returns 500 when database fails"– Error handling
❌ Avoid redundant tests:
- Don't test multiple validation error variants (missing field A vs. missing field B)
- Don't test Zod schemas in isolation – they're already tested by Zod
- Don't separate status code checks from schema checks
Test naming format: [action] [expected result] or [action] [condition]
Unit Tests (Helpers & Services)
For pure functions in app/helpers/ and app/services/, we write classic
unit tests. These focus on input/output behavior without HTTP concerns.
Location
Tests are co-located with source files:
app/helpers/
├── get-language.ts # Helper function
└── get-language.test.ts # Tests
app/services/
├── auth-middleware.server.ts # Service
└── auth-middleware.server.test.ts # Tests
Testing Focus
- Edge cases – E.g. empty inputs, boundary values, unexpected formats
- Error handling – Thrown errors, error messages
- Return values – Correct output for given input
Guidelines
- Prefer testing public API over internal implementation details
- Use descriptive test names that explain the scenario
- Group related tests with
describeblocks - No mocking needed for pure functions; mock only external dependencies
E2E Tests (Playwright)
E2E tests make real HTTP requests against a running server with a real database. They verify complete user workflows and critical paths.
When to Write E2E Tests
- Happy paths – Verify the main success scenarios work end-to-end
- Critical business logic – Only test edge cases that are essential to verify with real infrastructure
Test Files
tests/e2e/
├── smoke.test.ts # Critical path tests
└── admin/
└── options.test.ts
Running E2E Tests
pnpm run integration # All E2E tests
pnpm run integration tests/e2e/smoke.test.ts # Specific file