Testing Guidelines
Unit tests for logic, integration tests for workflows, E2E tests for user journeys. Use real implementations — only mock external paid APIs or rate-limited services.
Test Types
| Type | Location | Framework | Speed | What to test |
|---|---|---|---|---|
| Unit | tests/unit/ | Vitest | < 100ms | Pure functions, validation, parsing, formatting |
| Integration | tests/integration/ | Vitest + PostgreSQL | seconds | API endpoints, job processing, access control |
| E2E | tests/e2e/ | Playwright | 10-30s | Complete user workflows in the browser |
Running Tests
# From project root
make test # All tests
make test-ai # AI-friendly JSON output
make test-ai FILTER=date.test # Single file (24-120x faster)
make test-ai FILTER=tests/unit # Directory
make test-e2e # Playwright E2E tests
# From apps/web
pnpm test # All tests
pnpm test:unit # Unit only
pnpm test:integration # Integration only
pnpm test:e2e # E2E testsResults are saved as timestamped JSON in apps/web/.test-results/.
Mocking Rules
Never Mock
- Database operations — use test database
- Payload CMS — use real Payload
- Internal services — use actual implementations
- File system — use temp directories
- Job queues — use real handlers
Can Mock (Document Why)
- External paid APIs (Google Maps) — costs and rate limits
- Rate-limited services — avoid CI quotas
- Network failures — test error handling
- Time —
vi.setSystemTime()for date-dependent tests
Integration Test Setup
Use createIntegrationTestEnvironment() for tests that need a real database:
import { createIntegrationTestEnvironment, withCatalog, withDataset } from "@/tests/setup/integration-test-environment";
let testEnv: Awaited<ReturnType<typeof createIntegrationTestEnvironment>>;
beforeAll(async () => {
testEnv = await createIntegrationTestEnvironment();
});
afterAll(async () => {
await testEnv.cleanup();
});
beforeEach(async () => {
await testEnv.seedManager.truncate();
});
it("processes import file", async () => {
const { catalog } = await withCatalog(testEnv);
const { dataset } = await withDataset(testEnv, catalog.id);
// Test with real database and Payload...
});Databases are isolated per worker and cleaned up automatically.
Unit Test Setup
Use factories and mocks for business logic:
import { vi } from "vitest";
import { createEvent } from "@/tests/setup/factories";
const mockPayload = { findByID: vi.fn(), create: vi.fn() };
it("validates event data", () => {
const event = createEvent({ data: { title: "Test" } });
expect(validateEvent(event).valid).toBe(true);
});Test Credentials
Always use centralized test credentials to avoid hardcoded secrets:
import { TEST_CREDENTIALS, TEST_EMAILS } from "../constants/test-credentials";
const testUser = await payload.create({
collection: "users",
data: { email: TEST_EMAILS.admin, password: TEST_CREDENTIALS.basic.password, role: "admin" },
});Analyzing Failures
# Failed test names
cat apps/web/.test-results/$(ls -t apps/web/.test-results/ | head -1) | jq '.testResults[] | select(.status=="failed") | .name'
# Lint errors
cat apps/web/.lint-results/$(ls -t apps/web/.lint-results/ | head -1) | jq '.[] | select(.errorCount > 0) | .filePath'
# TypeScript errors
cat apps/web/.typecheck-results/$(ls -t apps/web/.typecheck-results/ | head -1) | jq '.errors[] | {file, line, code, message}'Last updated on