Testing Overview
Multi-layered testing approach for production applications
The testing architecture prevents bugs through automated validation at multiple levels. The system uses Vitest v3 for fast feedback, React Testing Library for component behavior, and Playwright for browser testing.
Pro Tip
Testing philosophy: Unit > Integration > E2E. Write tests that validate behavior through semantic assertions. Keep tests fast and deterministic.
Test Types
Tests live next to the code they validate, making them easy to find and update:
Unit Tests
*.unit.test.ts[x]
- Isolated component and function testing with minimal mocks
Integration Tests
*.integration.test.ts[x]
- Server components, API routes, and full feature flows
E2E Tests
e2e/*.spec.ts
- Browser automation for critical user journeys
Configuration
Vitest configuration with React and TypeScript support:
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
},
});
Heads up
Tests requiring Node.js APIs (API routes, server actions, middleware) must include /** @vitest-environment node */
at the top of the file.
Test Utilities
The @workspace/app-shell/lib/test-utils
package provides comprehensive testing utilities:
Component Testing
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { TestServicesProvider } from "@workspace/app-shell/lib/test-utils";
import { MyComponent } from "../my-component";
test("renders component with services", () => {
render(
<TestServicesProvider>
<MyComponent />
</TestServicesProvider>
);
expect(screen.getByText("Hello")).toBeInTheDocument();
});
Service Mocking
import { render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { TestServicesProvider } from "@workspace/app-shell/lib/test-utils";
test("shows upgrade prompt when no subscription", () => {
const mockBilling = {
useCustomer: vi.fn().mockReturnValue({
customer: null,
loading: false,
}),
checkout: vi.fn(),
attach: vi.fn(),
check: vi.fn(),
};
render(
<TestServicesProvider services={{ billing: mockBilling }}>
<DashboardGate />
</TestServicesProvider>
);
expect(screen.getByText(/upgrade/i)).toBeInTheDocument();
});
Assertion Helpers
import { expect, test } from "vitest";
import {
assertAuthHardenerHeaders,
assertRateLimitHeaders
} from "@workspace/app-shell/lib/test-utils";
test("API route has security headers", async () => {
const response = await fetch("/api/user");
// Validate CSRF protection, HSTS, cache controls, Vary headers
assertAuthHardenerHeaders(response.headers);
});
Running Tests
# Run all tests once
pnpm test
# Watch mode for development
pnpm test --watch
# Run specific file
pnpm test path/to/file.test.ts
# Run with verbose output
pnpm test --reporter=verbose
# Generate coverage report
pnpm test --coverage
# View in browser
open coverage/index.html
# Run only unit tests
pnpm test --include "**/*.unit.test.ts*"
# Run only integration tests
pnpm test --include "**/*.integration.test.ts*"
# Run tests matching pattern
pnpm test -t "authentication"
Development Workflow
Write the Feature
Create your component, API route, or Convex function using established patterns.
Write Tests
Use test utilities from @workspace/app-shell/lib/test-utils
:
import { render, screen, expect, test } from "@workspace/app-shell/lib/test-utils";
AI can generate tests if you reference llms/TESTING.md
.
Validate Before Commit
pnpm validate
Runs lint, typecheck, test, and build. Must pass before committing.
Test Organization
Tests live alongside the code they test:
apps/dashboard/
├── app/
│ └── dashboard/
│ ├── page.tsx
│ └── __tests__/
│ ├── page.unit.test.tsx
│ └── page.integration.test.tsx
├── components/
│ └── auth/
│ ├── user-menu.tsx
│ └── __tests__/
│ └── user-menu.test.tsx
Pro Tip
Co-location makes tests easy to find and update when code changes. No need to navigate deep test directories.
Security Testing
All tests validate security requirements automatically:
import { expect, test } from "vitest";
test("user API has security headers", async () => {
const response = await fetch("/api/user");
expect(response.headers.get("cache-control")).toContain("private, no-store");
expect(response.headers.get("vary")).toContain("Cookie");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
});
test("rejects unauthenticated requests", async () => {
const response = await fetch("/api/user");
expect(response.status).toBe(401);
});
Use assertion helpers for comprehensive checks:
import { assertAuthHardenerHeaders } from "@workspace/app-shell/lib/test-utils";
test("validates all security headers", async () => {
const response = await fetch("/api/protected");
assertAuthHardenerHeaders(response.headers);
});
Common Patterns
CI/CD Integration
Tests run automatically in CI pipeline:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test
env:
SKIP_ENV_VALIDATION: true
- name: Upload coverage
uses: codecov/codecov-action@v3
Pro Tip
Use SKIP_ENV_VALIDATION=true
in CI to run tests without real API keys. The codebase supports placeholder values for testing.
Related Documentation
- Unit Tests - Component and function testing
- Integration Tests - Feature flow testing
- E2E Tests - Browser automation
- AI Patterns - AI-assisted test generation