Skip to content

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:

  1. Happy path – Returns correct data matching the OpenAPI schema
  2. Validation errors – E.g. return 400 for invalid input
  3. 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 describe blocks
  • 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