StarterApp Docs
Testing

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:

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.

Run in Watch Mode

pnpm test --watch

Tests re-run automatically as you edit code.

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.