# Testing Reference Complete configurations, project structures, and setup guides for Grey Haven testing infrastructure. ## Table of Contents - [TypeScript Configuration](#typescript-configuration) - [Python Configuration](#python-configuration) - [Project Structures](#project-structures) - [Doppler Configuration](#doppler-configuration) - [GitHub Actions Configuration](#github-actions-configuration) - [Coverage Configuration](#coverage-configuration) ## TypeScript Configuration ### Complete vitest.config.ts ```typescript // vitest.config.ts import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import path from "path"; export default defineConfig({ plugins: [react()], test: { // Enable global test APIs (describe, it, expect) globals: true, // Use jsdom for browser-like environment environment: "jsdom", // Run setup file before tests setupFiles: ["./tests/setup.ts"], // Coverage configuration coverage: { // Use V8 coverage provider (faster than Istanbul) provider: "v8", // Coverage reporters reporter: ["text", "json", "html"], // Exclude from coverage exclude: [ "node_modules/", "tests/", "**/*.config.ts", "**/*.d.ts", "**/types/", "**/__mocks__/", ], // Minimum coverage thresholds (enforced in CI) thresholds: { lines: 80, functions: 80, branches: 80, statements: 80, }, }, // Environment variables for tests env: { // Doppler provides these at runtime DATABASE_URL_ADMIN: process.env.DATABASE_URL_ADMIN || "postgresql://localhost/test", REDIS_URL: process.env.REDIS_URL || "redis://localhost:6379", VITE_API_URL: process.env.VITE_API_URL || "http://localhost:3000", }, // Test timeout (ms) testTimeout: 10000, // Hook timeouts hookTimeout: 10000, // Retry failed tests retry: 0, // Run tests in parallel threads: true, // Maximum concurrent threads maxThreads: 4, // Minimum concurrent threads minThreads: 1, }, // Path aliases resolve: { alias: { "~": path.resolve(__dirname, "./src"), }, }, }); ``` **Field Explanations:** - `globals: true` - Makes test APIs available without imports - `environment: "jsdom"` - Simulates browser environment for React components - `setupFiles` - Runs before each test file - `coverage.provider: "v8"` - Fast coverage using V8 engine - `coverage.thresholds` - Enforces minimum coverage percentages - `testTimeout: 10000` - Each test must complete within 10 seconds - `threads: true` - Run tests in parallel for speed - `retry: 0` - Don't retry failed tests (fail fast) ### Test Setup File (tests/setup.ts) ```typescript // tests/setup.ts import { afterEach, beforeAll, afterAll, vi } from "vitest"; import { cleanup } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; // Cleanup after each test case afterEach(() => { cleanup(); vi.clearAllMocks(); }); // Setup before all tests beforeAll(() => { // Mock environment variables process.env.VITE_API_URL = "http://localhost:3000"; process.env.DATABASE_URL_ADMIN = "postgresql://localhost/test"; // Mock window.matchMedia (for responsive components) Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); // Mock IntersectionObserver global.IntersectionObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })); }); // Cleanup after all tests afterAll(async () => { // Close database connections // Clean up any resources }); ``` ### Package.json Scripts ```json { "scripts": { "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "test:unit": "vitest run tests/unit", "test:integration": "vitest run tests/integration", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, "devDependencies": { "@playwright/test": "^1.40.0", "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", "@vitest/ui": "^1.0.4", "@faker-js/faker": "^8.3.1", "vitest": "^1.0.4", "@vitest/coverage-v8": "^1.0.4" } } ``` ### Playwright Configuration ```typescript // playwright.config.ts import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./tests/e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: "html", use: { baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000", trace: "on-first-retry", }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, ], webServer: { command: "bun run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, }); ``` ## Python Configuration ### Complete pyproject.toml ```toml # pyproject.toml [tool.pytest.ini_options] # Test discovery testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] # Command line options addopts = [ "--strict-markers", # Error on unknown markers "--strict-config", # Error on config errors "-ra", # Show extra test summary "--cov=app", # Measure coverage of app/ directory "--cov-report=term-missing", # Show missing lines in terminal "--cov-report=html", # Generate HTML coverage report "--cov-report=xml", # Generate XML for CI tools "--cov-fail-under=80", # Fail if coverage < 80% "-v", # Verbose output ] # Test markers (use with @pytest.mark.unit, etc.) markers = [ "unit: Fast, isolated unit tests", "integration: Tests involving multiple components", "e2e: End-to-end tests through full flows", "benchmark: Performance tests", "slow: Tests that take >5 seconds", ] # Async support asyncio_mode = "auto" # Test output console_output_style = "progress" # Warnings filterwarnings = [ "error", # Treat warnings as errors "ignore::DeprecationWarning", # Ignore deprecation warnings "ignore::PendingDeprecationWarning", # Ignore pending deprecations ] # Coverage configuration [tool.coverage.run] source = ["app"] omit = [ "*/tests/*", "*/conftest.py", "*/__init__.py", "*/migrations/*", "*/config/*", ] branch = true parallel = true [tool.coverage.report] precision = 2 show_missing = true skip_covered = false exclude_lines = [ "pragma: no cover", "def __repr__", "def __str__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "class .*\\bProtocol\\):", "@(abc\\.)?abstractmethod", ] [tool.coverage.html] directory = "htmlcov" [tool.coverage.xml] output = "coverage.xml" ``` **Configuration Explanations:** - `testpaths = ["tests"]` - Only look for tests in tests/ directory - `--strict-markers` - Fail if test uses undefined marker - `--cov=app` - Measure coverage of app/ directory - `--cov-fail-under=80` - CI fails if coverage < 80% - `asyncio_mode = "auto"` - Auto-detect async tests - `branch = true` - Measure branch coverage (more thorough) - `parallel = true` - Support parallel test execution ### Development Dependencies ```txt # requirements-dev.txt # Testing pytest==8.0.0 pytest-asyncio==0.23.3 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-benchmark==4.0.0 # Test utilities faker==22.0.0 factory-boy==3.3.0 httpx==0.26.0 # Type checking mypy==1.8.0 # Linting ruff==0.1.9 # Task runner taskipy==1.12.2 ``` ### Taskfile Configuration ```toml # pyproject.toml (continued) [tool.taskipy.tasks] # Testing tasks test = "doppler run -- pytest" test-unit = "doppler run -- pytest -m unit" test-integration = "doppler run -- pytest -m integration" test-e2e = "doppler run -- pytest -m e2e" test-benchmark = "doppler run -- pytest -m benchmark" test-coverage = "doppler run -- pytest --cov=app --cov-report=html" test-watch = "doppler run -- pytest-watch" # Linting and formatting lint = "ruff check app tests" format = "ruff format app tests" typecheck = "mypy app" # Combined checks check = "task lint && task typecheck && task test" ``` ## Project Structures ### TypeScript Project Structure ```plaintext project-root/ ├── src/ │ ├── routes/ # TanStack Router pages │ │ ├── index.tsx │ │ ├── settings/ │ │ │ ├── profile.tsx │ │ │ └── account.tsx │ │ └── __root.tsx │ ├── lib/ │ │ ├── components/ # React components │ │ │ ├── auth/ │ │ │ │ ├── provider.tsx │ │ │ │ └── login-form.tsx │ │ │ ├── ui/ # UI primitives (shadcn) │ │ │ │ ├── button.tsx │ │ │ │ └── input.tsx │ │ │ └── UserProfile.tsx │ │ ├── server/ # Server-side code │ │ │ ├── db/ │ │ │ │ ├── schema.ts # Drizzle schema │ │ │ │ └── index.ts # DB connection │ │ │ └── functions/ # Server functions │ │ │ ├── users.ts │ │ │ └── auth.ts │ │ ├── hooks/ # Custom React hooks │ │ │ ├── use-auth.ts │ │ │ └── use-users.ts │ │ ├── utils/ # Utility functions │ │ │ ├── format.ts │ │ │ └── validation.ts │ │ └── types/ # TypeScript types │ │ ├── user.ts │ │ └── api.ts │ └── public/ # Static assets │ └── favicon.ico ├── tests/ │ ├── setup.ts # Test setup │ ├── unit/ # Unit tests │ │ ├── lib/ │ │ │ ├── components/ │ │ │ │ └── UserProfile.test.tsx │ │ │ └── utils/ │ │ │ └── format.test.ts │ │ └── server/ │ │ └── functions/ │ │ └── users.test.ts │ ├── integration/ # Integration tests │ │ ├── auth-flow.test.ts │ │ └── user-repository.test.ts │ ├── e2e/ # Playwright E2E tests │ │ ├── user-registration.spec.ts │ │ └── user-workflow.spec.ts │ └── factories/ # Test data factories │ ├── user.factory.ts │ └── tenant.factory.ts ├── vitest.config.ts # Vitest configuration ├── playwright.config.ts # Playwright configuration ├── package.json └── tsconfig.json ``` ### Python Project Structure ```plaintext project-root/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI application │ ├── config/ │ │ ├── __init__.py │ │ └── settings.py # Application settings │ ├── db/ │ │ ├── __init__.py │ │ ├── base.py # Database connection │ │ ├── models/ # SQLModel entities │ │ │ ├── __init__.py │ │ │ ├── base.py # Base model │ │ │ ├── user.py │ │ │ └── tenant.py │ │ └── repositories/ # Repository pattern │ │ ├── __init__.py │ │ ├── base.py # Base repository │ │ └── user_repository.py │ ├── routers/ # FastAPI endpoints │ │ ├── __init__.py │ │ ├── users.py │ │ └── auth.py │ ├── services/ # Business logic │ │ ├── __init__.py │ │ ├── user_service.py │ │ └── auth_service.py │ ├── schemas/ # Pydantic schemas (API contracts) │ │ ├── __init__.py │ │ ├── user.py │ │ └── auth.py │ └── utils/ # Utilities │ ├── __init__.py │ ├── security.py │ └── validation.py ├── tests/ │ ├── __init__.py │ ├── conftest.py # Shared fixtures │ ├── unit/ # Unit tests (@pytest.mark.unit) │ │ ├── __init__.py │ │ ├── repositories/ │ │ │ └── test_user_repository.py │ │ └── services/ │ │ └── test_user_service.py │ ├── integration/ # Integration tests │ │ ├── __init__.py │ │ └── test_user_api.py │ ├── e2e/ # E2E tests │ │ ├── __init__.py │ │ └── test_full_user_flow.py │ ├── benchmark/ # Benchmark tests │ │ ├── __init__.py │ │ └── test_repository_performance.py │ └── factories/ # Test data factories │ ├── __init__.py │ └── user_factory.py ├── pyproject.toml # Python project config ├── requirements.txt # Production dependencies ├── requirements-dev.txt # Development dependencies └── .python-version # Python version (3.12) ``` ## Doppler Configuration ### Doppler Setup ```bash # Install Doppler CLI brew install dopplerhq/cli/doppler # macOS # or curl -Ls https://cli.doppler.com/install.sh | sh # Linux # Authenticate with Doppler doppler login # Setup Doppler in project doppler setup # Select project and config # Project: your-project-name # Config: test (or dev, staging, production) ``` ### Doppler Environment Configs Grey Haven projects use these Doppler configs: 1. **dev** - Local development environment 2. **test** - Running tests (CI and local) 3. **staging** - Staging environment 4. **production** - Production environment ### Test Environment Variables **Database URLs:** ```bash # PostgreSQL connection URLs (Doppler managed) DATABASE_URL_ADMIN=postgresql+asyncpg://admin_user:password@localhost:5432/app_db DATABASE_URL_AUTHENTICATED=postgresql+asyncpg://authenticated_user:password@localhost:5432/app_db DATABASE_URL_ANON=postgresql+asyncpg://anon_user:password@localhost:5432/app_db # Test database (separate from dev) DATABASE_URL_TEST=postgresql+asyncpg://test_user:password@localhost:5432/test_db ``` **Redis:** ```bash # Use separate Redis DB for tests (0-15 available) REDIS_URL=redis://localhost:6379/1 # DB 1 for tests (dev uses 0) ``` **Authentication:** ```bash # Better Auth secrets BETTER_AUTH_SECRET=test-secret-key-min-32-chars-long BETTER_AUTH_URL=http://localhost:3000 # JWT secrets JWT_SECRET_KEY=test-jwt-secret-key ``` **External Services (use test/sandbox keys):** ```bash # Stripe (test mode) STRIPE_SECRET_KEY=sk_test_51AbCdEfGhIjKlMnOpQrStUv STRIPE_PUBLISHABLE_KEY=pk_test_51AbCdEfGhIjKlMnOpQrStUv # Resend (test mode) RESEND_API_KEY=re_test_1234567890abcdef # OpenAI (separate test key) OPENAI_API_KEY=sk-test-1234567890abcdef ``` **E2E Testing:** ```bash # Playwright base URL PLAYWRIGHT_BASE_URL=http://localhost:3000 # Email testing service (for E2E tests) MAILTRAP_API_TOKEN=your_mailtrap_token ``` ### Running Tests with Doppler **TypeScript:** ```bash # Run all tests with Doppler doppler run -- bun run test # Run with specific config doppler run --config test -- bun run test # Run coverage doppler run -- bun run test:coverage # Run E2E doppler run -- bun run test:e2e ``` **Python:** ```bash # Activate virtual environment first! source .venv/bin/activate # Run all tests with Doppler doppler run -- pytest # Run with specific config doppler run --config test -- pytest # Run specific markers doppler run -- pytest -m unit doppler run -- pytest -m integration ``` ### Doppler in CI/CD **GitHub Actions:** ```yaml - name: Install Doppler CLI uses: dopplerhq/cli-action@v3 - name: Run tests with Doppler env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: doppler run --config test -- bun run test:coverage ``` **Get Doppler Service Token:** 1. Go to Doppler dashboard 2. Select your project 3. Go to Access → Service Tokens 4. Create token for `test` config 5. Add as `DOPPLER_TOKEN_TEST` secret in GitHub ## GitHub Actions Configuration ### TypeScript CI Workflow ```yaml # .github/workflows/test-typescript.yml name: TypeScript Tests on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_DB: test_db POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 redis: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" cache: "bun" - name: Install Doppler CLI uses: dopplerhq/cli-action@v3 - name: Install dependencies run: bun install - name: Run linter run: bun run lint - name: Run type check run: bun run typecheck - name: Run unit tests env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: doppler run --config test -- bun run test:unit - name: Run integration tests env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: doppler run --config test -- bun run test:integration - name: Run tests with coverage env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: doppler run --config test -- bun run test:coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: files: ./coverage/coverage-final.json flags: typescript name: typescript-coverage - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run E2E tests env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: doppler run --config test -- bun run test:e2e - name: Upload Playwright report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 30 ``` ### Python CI Workflow ```yaml # .github/workflows/test-python.yml name: Python Tests on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_DB: test_db POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 redis: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" cache: "pip" - name: Install Doppler CLI uses: dopplerhq/cli-action@v3 - name: Create virtual environment run: python -m venv .venv - name: Install dependencies run: | source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txt -r requirements-dev.txt - name: Run linter run: | source .venv/bin/activate ruff check app tests - name: Run type checker run: | source .venv/bin/activate mypy app - name: Run unit tests env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: | source .venv/bin/activate doppler run --config test -- pytest -m unit - name: Run integration tests env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: | source .venv/bin/activate doppler run --config test -- pytest -m integration - name: Run all tests with coverage env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }} run: | source .venv/bin/activate doppler run --config test -- pytest --cov=app --cov-report=xml --cov-report=html - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: files: ./coverage.xml flags: python name: python-coverage - name: Upload coverage HTML if: always() uses: actions/upload-artifact@v4 with: name: coverage-report path: htmlcov/ retention-days: 30 ``` ## Coverage Configuration ### Coverage Thresholds **Minimum requirements (enforced in CI):** - **Lines:** 80% - **Functions:** 80% - **Branches:** 80% - **Statements:** 80% **Target goals:** - **Critical paths:** 90%+ - **Security code:** 100% (auth, payments, tenant isolation) - **Utility functions:** 95%+ ### Excluding from Coverage **TypeScript (vitest.config.ts):** ```typescript coverage: { exclude: [ "node_modules/", "tests/", "**/*.config.ts", "**/*.d.ts", "**/types/", "**/__mocks__/", "**/migrations/", ], } ``` **Python (pyproject.toml):** ```toml [tool.coverage.run] omit = [ "*/tests/*", "*/conftest.py", "*/__init__.py", "*/migrations/*", "*/config/*", ] ``` ### Coverage Reports **Viewing coverage locally:** ```bash # TypeScript bun run test:coverage open coverage/index.html # Python source .venv/bin/activate doppler run -- pytest --cov=app --cov-report=html open htmlcov/index.html ``` **Coverage in CI:** - Upload to Codecov for tracking over time - Fail build if coverage < 80% - Comment coverage diff on PRs - Track coverage trends ### Pre-commit Hook for Coverage ```yaml # .pre-commit-config.yaml repos: - repo: local hooks: - id: test-coverage name: Check test coverage entry: sh -c 'source .venv/bin/activate && pytest --cov=app --cov-fail-under=80' language: system pass_filenames: false always_run: true ```