Initial commit
This commit is contained in:
946
skills/testing-strategy/REFERENCE.md
Normal file
946
skills/testing-strategy/REFERENCE.md
Normal file
@@ -0,0 +1,946 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user