# Testing Examples
Copy-paste ready test examples from Grey Haven Studio production templates.
## Table of Contents
- [Vitest Examples](#vitest-examples)
- [Unit Tests](#vitest-unit-tests)
- [Component Tests](#vitest-component-tests)
- [Integration Tests](#vitest-integration-tests)
- [E2E Tests](#vitest-e2e-tests)
- [Pytest Examples](#pytest-examples)
- [Unit Tests](#pytest-unit-tests)
- [Integration Tests](#pytest-integration-tests)
- [E2E Tests](#pytest-e2e-tests)
- [Benchmark Tests](#pytest-benchmark-tests)
- [Test Factories and Fixtures](#test-factories-and-fixtures)
## Vitest Examples
### Vitest Unit Tests
**Testing utility functions:**
```typescript
// tests/unit/lib/utils/format.test.ts
import { describe, it, expect } from "vitest";
import { formatDate, formatCurrency } from "~/lib/utils/format";
describe("formatDate", () => {
it("formats ISO date to readable format", () => {
const date = new Date("2025-10-20T12:00:00Z");
expect(formatDate(date)).toBe("Oct 20, 2025");
});
it("handles null dates", () => {
expect(formatDate(null)).toBe("N/A");
});
});
describe("formatCurrency", () => {
it("formats USD currency with 2 decimals", () => {
expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
});
it("handles zero values", () => {
expect(formatCurrency(0, "USD")).toBe("$0.00");
});
});
```
**Testing business logic:**
```typescript
// tests/unit/lib/utils/validation.test.ts
import { describe, it, expect } from "vitest";
import { validateEmail, validatePassword } from "~/lib/utils/validation";
describe("validateEmail", () => {
it("accepts valid email addresses", () => {
expect(validateEmail("user@example.com")).toBe(true);
expect(validateEmail("test.user+tag@example.co.uk")).toBe(true);
});
it("rejects invalid email addresses", () => {
expect(validateEmail("invalid")).toBe(false);
expect(validateEmail("@example.com")).toBe(false);
expect(validateEmail("user@")).toBe(false);
});
});
describe("validatePassword", () => {
it("requires minimum length of 8 characters", () => {
expect(validatePassword("short")).toBe(false);
expect(validatePassword("longenough")).toBe(true);
});
it("requires at least one number", () => {
expect(validatePassword("noNumbers")).toBe(false);
expect(validatePassword("hasNumber1")).toBe(true);
});
});
```
### Vitest Component Tests
**Testing React components with React Testing Library:**
```typescript
// tests/unit/lib/components/UserProfile.test.tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import UserProfile from "~/lib/components/UserProfile";
import * as userFunctions from "~/lib/server/functions/users";
// Mock server functions
vi.mock("~/lib/server/functions/users");
describe("UserProfile", () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
it("displays user name when loaded", async () => {
const mockUser = {
id: "123",
name: "John Doe",
email: "john@example.com",
};
vi.mocked(userFunctions.getUserById).mockResolvedValue(mockUser);
render(, { wrapper });
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
});
it("displays loading state initially", () => {
vi.mocked(userFunctions.getUserById).mockImplementation(
() => new Promise(() => {}) // Never resolves
);
render(, { wrapper });
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it("displays error message on fetch failure", async () => {
vi.mocked(userFunctions.getUserById).mockRejectedValue(
new Error("Network error")
);
render(, { wrapper });
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
```
**Testing user interactions:**
```typescript
// tests/unit/lib/components/Counter.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "~/lib/components/Counter";
describe("Counter", () => {
it("starts at zero", () => {
render();
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("increments when button clicked", () => {
render();
const button = screen.getByRole("button", { name: /increment/i });
fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText("Count: 2")).toBeInTheDocument();
});
it("decrements when decrement button clicked", () => {
render();
const increment = screen.getByRole("button", { name: /increment/i });
const decrement = screen.getByRole("button", { name: /decrement/i });
fireEvent.click(increment);
fireEvent.click(increment);
fireEvent.click(decrement);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
});
```
### Vitest Integration Tests
**Testing TanStack Query with server functions:**
```typescript
// tests/integration/auth-flow.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { QueryClient } from "@tanstack/react-query";
import { login, logout, getCurrentUser } from "~/lib/server/functions/auth";
describe("Authentication Flow", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
});
it("completes full login and logout cycle", async () => {
// Login
const loginResult = await login({
email: "test@example.com",
password: "password123",
});
expect(loginResult.success).toBe(true);
expect(loginResult.user).toBeDefined();
// Verify session
const user = await getCurrentUser();
expect(user.email).toBe("test@example.com");
// Logout
const logoutResult = await logout();
expect(logoutResult.success).toBe(true);
// Verify session cleared
const userAfterLogout = await getCurrentUser();
expect(userAfterLogout).toBeNull();
});
});
```
**Testing database operations:**
```typescript
// tests/integration/user-repository.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { db } from "~/lib/server/db";
import { users } from "~/lib/server/db/schema";
import { eq } from "drizzle-orm";
describe("User Repository Integration", () => {
const testTenantId = "550e8400-e29b-41d4-a716-446655440000";
beforeEach(async () => {
// Clean up test data
await db.delete(users).where(eq(users.tenant_id, testTenantId));
});
it("creates and retrieves user with tenant isolation", async () => {
// Create user
const [user] = await db
.insert(users)
.values({
tenant_id: testTenantId,
email_address: "test@example.com",
name: "Test User",
})
.returning();
expect(user).toBeDefined();
expect(user.email_address).toBe("test@example.com");
// Retrieve user
const [retrieved] = await db
.select()
.from(users)
.where(eq(users.id, user.id))
.where(eq(users.tenant_id, testTenantId));
expect(retrieved).toBeDefined();
expect(retrieved.id).toBe(user.id);
});
});
```
### Vitest E2E Tests
**Testing with Playwright:**
```typescript
// tests/e2e/user-registration.spec.ts
import { test, expect } from "@playwright/test";
// Doppler provides PLAYWRIGHT_BASE_URL
const baseUrl = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000";
test.describe("User Registration", () => {
test("completes registration with magic link", async ({ page }) => {
await page.goto(`${baseUrl}/auth/signup`);
// Fill registration form
await page.fill('input[name="email"]', "newuser@example.com");
await page.fill('input[name="name"]', "New User");
await page.click('button[type="submit"]');
// Verify email sent message
await expect(page.locator("text=Check your email")).toBeVisible();
// Simulate magic link click (in real test, check email)
// This would use email testing service in CI
});
test("validates email format", async ({ page }) => {
await page.goto(`${baseUrl}/auth/signup`);
await page.fill('input[name="email"]', "invalid-email");
await page.click('button[type="submit"]');
await expect(page.locator("text=Invalid email")).toBeVisible();
});
});
```
**Testing full user workflows:**
```typescript
// tests/e2e/user-workflow.spec.ts
import { test, expect } from "@playwright/test";
const baseUrl = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000";
test.describe("User Workflow", () => {
test("complete user profile update flow", async ({ page }) => {
// 1. Login
await page.goto(`${baseUrl}/auth/login`);
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
// 2. Navigate to profile
await page.click('a[href="/settings/profile"]');
await expect(page).toHaveURL(`${baseUrl}/settings/profile`);
// 3. Update profile
await page.fill('input[name="name"]', "Updated Name");
await page.click('button:has-text("Save")');
// 4. Verify update
await expect(page.locator("text=Profile updated")).toBeVisible();
await expect(page.locator('input[name="name"]')).toHaveValue("Updated Name");
});
});
```
## Pytest Examples
### Pytest Unit Tests
**Testing repository with tenant isolation:**
```python
# tests/unit/repositories/test_user_repository.py
import pytest
from uuid import uuid4
from app.db.repositories.user_repository import UserRepository
from app.db.models.user import User
@pytest.mark.unit
class TestUserRepository:
"""Unit tests for UserRepository."""
async def test_get_by_id_with_tenant_isolation(
self, session, tenant_id, test_user
):
"""Test get_by_id enforces tenant isolation."""
repo = UserRepository(session)
# Should find user with correct tenant_id
user = await repo.get_by_id(test_user.id, tenant_id)
assert user is not None
assert user.id == test_user.id
# Should NOT find user with different tenant_id
different_tenant = uuid4()
user = await repo.get_by_id(test_user.id, different_tenant)
assert user is None
async def test_list_with_pagination(self, session, tenant_id):
"""Test list with limit and offset."""
repo = UserRepository(session)
# Create multiple users
for i in range(10):
user = User(
tenant_id=tenant_id,
email_address=f"user{i}@example.com",
name=f"User {i}",
)
session.add(user)
await session.commit()
# Test pagination
page1 = await repo.list(tenant_id, limit=5, offset=0)
assert len(page1) == 5
page2 = await repo.list(tenant_id, limit=5, offset=5)
assert len(page2) == 5
# Pages should not overlap
page1_ids = {u.id for u in page1}
page2_ids = {u.id for u in page2}
assert page1_ids.isdisjoint(page2_ids)
```
**Testing service layer:**
```python
# tests/unit/services/test_user_service.py
import pytest
from uuid import uuid4
from unittest.mock import AsyncMock, MagicMock
from app.services.user_service import UserService
from app.db.models.user import User
@pytest.mark.unit
class TestUserService:
"""Unit tests for UserService."""
async def test_create_user_success(self, tenant_id):
"""Test creating a new user."""
# Mock repository
mock_repo = AsyncMock()
mock_repo.create.return_value = User(
id=uuid4(),
tenant_id=tenant_id,
email_address="new@example.com",
name="New User",
)
# Create service with mocked repo
service = UserService(mock_repo)
# Call method
user = await service.create_user(
tenant_id=tenant_id,
email="new@example.com",
name="New User",
)
# Verify
assert user.email_address == "new@example.com"
mock_repo.create.assert_called_once()
async def test_create_user_duplicate_email(self, tenant_id):
"""Test creating user with duplicate email raises error."""
mock_repo = AsyncMock()
mock_repo.create.side_effect = ValueError("Email already exists")
service = UserService(mock_repo)
with pytest.raises(ValueError, match="Email already exists"):
await service.create_user(
tenant_id=tenant_id,
email="duplicate@example.com",
name="Duplicate User",
)
```
### Pytest Integration Tests
**Testing FastAPI endpoints:**
```python
# tests/integration/test_user_api.py
import pytest
from httpx import AsyncClient
@pytest.mark.integration
class TestUserAPI:
"""Integration tests for User API endpoints."""
async def test_create_user_endpoint(self, client: AsyncClient, tenant_id):
"""Test POST /users creates user with tenant isolation."""
response = await client.post(
"/api/users",
json={
"email_address": "newuser@example.com",
"name": "New User",
},
headers={"X-Tenant-ID": str(tenant_id)},
)
assert response.status_code == 201
data = response.json()
assert data["email_address"] == "newuser@example.com"
assert data["tenant_id"] == str(tenant_id)
async def test_get_user_enforces_tenant_isolation(
self, client: AsyncClient, test_user, tenant_id
):
"""Test GET /users/{id} enforces tenant isolation."""
# Should succeed with correct tenant
response = await client.get(
f"/api/users/{test_user.id}",
headers={"X-Tenant-ID": str(tenant_id)},
)
assert response.status_code == 200
# Should fail with different tenant
different_tenant = "00000000-0000-0000-0000-000000000000"
response = await client.get(
f"/api/users/{test_user.id}",
headers={"X-Tenant-ID": different_tenant},
)
assert response.status_code == 404
async def test_update_user_endpoint(
self, client: AsyncClient, test_user, tenant_id
):
"""Test PATCH /users/{id} updates user."""
response = await client.patch(
f"/api/users/{test_user.id}",
json={"name": "Updated Name"},
headers={"X-Tenant-ID": str(tenant_id)},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
assert data["id"] == str(test_user.id)
```
**Testing database transactions:**
```python
# tests/integration/test_transaction_handling.py
import pytest
from sqlalchemy.exc import IntegrityError
from app.db.repositories.user_repository import UserRepository
from app.db.models.user import User
@pytest.mark.integration
class TestTransactionHandling:
"""Integration tests for database transactions."""
async def test_rollback_on_error(self, session, tenant_id):
"""Test that errors cause transaction rollback."""
repo = UserRepository(session)
# Create first user successfully
user1 = User(
tenant_id=tenant_id,
email_address="user1@example.com",
name="User 1",
)
session.add(user1)
await session.commit()
# Try to create duplicate email (should fail)
with pytest.raises(IntegrityError):
user2 = User(
tenant_id=tenant_id,
email_address="user1@example.com", # Duplicate!
name="User 2",
)
session.add(user2)
await session.commit()
# Verify only first user exists
users = await repo.list(tenant_id)
assert len(users) == 1
assert users[0].email_address == "user1@example.com"
```
### Pytest E2E Tests
**Testing complete user lifecycle:**
```python
# tests/e2e/test_full_user_flow.py
import pytest
from httpx import AsyncClient
@pytest.mark.e2e
class TestUserFlowE2E:
"""End-to-end tests for complete user workflows."""
async def test_complete_user_lifecycle(self, client: AsyncClient, tenant_id):
"""Test create, read, update, delete user flow."""
headers = {"X-Tenant-ID": str(tenant_id)}
# 1. Create user
response = await client.post(
"/api/users",
json={"email_address": "lifecycle@example.com", "name": "Lifecycle User"},
headers=headers,
)
assert response.status_code == 201
user_id = response.json()["id"]
# 2. Read user
response = await client.get(f"/api/users/{user_id}", headers=headers)
assert response.status_code == 200
assert response.json()["name"] == "Lifecycle User"
# 3. Update user
response = await client.patch(
f"/api/users/{user_id}",
json={"name": "Updated User"},
headers=headers,
)
assert response.status_code == 200
assert response.json()["name"] == "Updated User"
# 4. Delete user
response = await client.delete(f"/api/users/{user_id}", headers=headers)
assert response.status_code == 204
# 5. Verify deleted
response = await client.get(f"/api/users/{user_id}", headers=headers)
assert response.status_code == 404
```
**Testing authentication flow:**
```python
# tests/e2e/test_auth_flow.py
import pytest
from httpx import AsyncClient
@pytest.mark.e2e
class TestAuthFlowE2E:
"""End-to-end tests for authentication workflows."""
async def test_registration_and_login_flow(self, client: AsyncClient):
"""Test complete registration and login flow."""
# 1. Register new user
response = await client.post(
"/api/auth/register",
json={
"email_address": "newuser@example.com",
"password": "SecurePass123!",
"name": "New User",
},
)
assert response.status_code == 201
# 2. Login with credentials
response = await client.post(
"/api/auth/login",
json={
"email_address": "newuser@example.com",
"password": "SecurePass123!",
},
)
assert response.status_code == 200
token = response.json()["access_token"]
# 3. Access protected endpoint
response = await client.get(
"/api/users/me",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
assert response.json()["email_address"] == "newuser@example.com"
# 4. Logout
response = await client.post(
"/api/auth/logout",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
```
### Pytest Benchmark Tests
**Benchmarking database queries:**
```python
# tests/benchmark/test_repository_performance.py
import pytest
from uuid import uuid4
from app.db.repositories.user_repository import UserRepository
@pytest.mark.benchmark
class TestRepositoryPerformance:
"""Benchmark tests for repository performance."""
async def test_list_query_performance(
self, session, tenant_id, benchmark
):
"""Benchmark list query with 1000 users."""
repo = UserRepository(session)
# Setup: Create 1000 users
from app.db.models.user import User
users = [
User(
tenant_id=tenant_id,
email_address=f"user{i}@example.com",
name=f"User {i}",
)
for i in range(1000)
]
session.add_all(users)
await session.commit()
# Benchmark the list query
result = await benchmark(repo.list, tenant_id, limit=50, offset=0)
assert len(result) == 50
# Should complete in < 100ms
assert benchmark.stats.mean < 0.1
async def test_bulk_insert_performance(self, session, tenant_id, benchmark):
"""Benchmark bulk insert operations."""
from app.db.models.user import User
def create_users():
users = [
User(
tenant_id=tenant_id,
email_address=f"bulk{i}@example.com",
name=f"Bulk User {i}",
)
for i in range(100)
]
session.add_all(users)
return users
result = benchmark(create_users)
assert len(result) == 100
```
## Test Factories and Fixtures
### TypeScript Factory Pattern
**User factory:**
```typescript
// tests/factories/user.factory.ts
import { faker } from "@faker-js/faker";
export function createMockUser(overrides = {}) {
return {
id: faker.string.uuid(),
created_at: faker.date.past(),
tenant_id: faker.string.uuid(),
email_address: faker.internet.email(),
name: faker.person.fullName(),
is_active: true,
...overrides,
};
}
export function createMockUsers(count: number, overrides = {}) {
return Array.from({ length: count }, () => createMockUser(overrides));
}
```
**Usage in tests:**
```typescript
// tests/unit/lib/components/UserList.test.tsx
import { createMockUsers } from "../../factories/user.factory";
it("displays list of users", () => {
const users = createMockUsers(5, { tenant_id: "test-tenant" });
render();
expect(screen.getAllByRole("listitem")).toHaveLength(5);
});
```
### Python Factory Pattern
**User factory:**
```python
# tests/factories/user_factory.py
from faker import Faker
from uuid import uuid4
from app.db.models.user import User
fake = Faker()
def create_user(tenant_id=None, **overrides):
"""Create test user with random data."""
defaults = {
"tenant_id": tenant_id or uuid4(),
"email_address": fake.email(),
"name": fake.name(),
"is_active": True,
}
return User(**{**defaults, **overrides})
def create_users(count: int, tenant_id=None, **overrides):
"""Create multiple test users."""
return [create_user(tenant_id=tenant_id, **overrides) for _ in range(count)]
```
**Usage in tests:**
```python
# tests/unit/test_user_service.py
from tests.factories.user_factory import create_users
async def test_bulk_user_creation(session, tenant_id):
"""Test creating multiple users."""
users = create_users(10, tenant_id=tenant_id)
session.add_all(users)
await session.commit()
# Verify all created with same tenant
for user in users:
assert user.tenant_id == tenant_id
```
### Python Pytest Fixtures
**Complete conftest.py example:**
```python
# tests/conftest.py
import pytest
import os
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from httpx import AsyncClient
from app.main import app
from app.db.models import Base
# Doppler provides DATABASE_URL_TEST at runtime
DATABASE_URL_TEST = os.getenv("DATABASE_URL_TEST", "postgresql+asyncpg://localhost/test")
@pytest.fixture(scope="session")
async def engine():
"""Create test database engine."""
engine = create_async_engine(DATABASE_URL_TEST, echo=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def session(engine):
"""Create test database session."""
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session
await session.rollback()
@pytest.fixture
async def client():
"""Create test HTTP client."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
def tenant_id():
"""Provide test tenant ID."""
return "550e8400-e29b-41d4-a716-446655440000"
@pytest.fixture
async def test_user(session, tenant_id):
"""Create test user."""
from app.db.models.user import User
user = User(
tenant_id=tenant_id,
email_address="test@example.com",
name="Test User",
is_active=True,
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
@pytest.fixture
async def authenticated_client(client, test_user):
"""Create authenticated HTTP client."""
# Login and get token
response = await client.post(
"/api/auth/login",
json={
"email_address": test_user.email_address,
"password": "testpassword",
},
)
token = response.json()["access_token"]
# Add auth header to client
client.headers["Authorization"] = f"Bearer {token}"
return client
```
## Mocking Patterns
### TypeScript Mocking (Vitest)
**Mocking modules:**
```typescript
// tests/unit/lib/services/api.test.ts
import { vi } from "vitest";
import * as apiClient from "~/lib/services/api-client";
// Mock entire module
vi.mock("~/lib/services/api-client", () => ({
fetchUser: vi.fn(),
updateUser: vi.fn(),
}));
it("calls API client with correct parameters", async () => {
vi.mocked(apiClient.fetchUser).mockResolvedValue({
id: "123",
name: "Test User",
});
const user = await apiClient.fetchUser("123");
expect(apiClient.fetchUser).toHaveBeenCalledWith("123");
expect(user.name).toBe("Test User");
});
```
**Mocking environment variables:**
```typescript
// tests/unit/lib/config/env.test.ts
import { vi } from "vitest";
it("loads environment variables", () => {
vi.stubEnv("VITE_API_URL", "https://test.example.com");
const config = loadConfig();
expect(config.apiUrl).toBe("https://test.example.com");
vi.unstubAllEnvs();
});
```
### Python Mocking (pytest)
**Mocking with unittest.mock:**
```python
# tests/unit/test_email_service.py
import pytest
from unittest.mock import AsyncMock, patch
from app.services.email_service import EmailService
@pytest.mark.unit
async def test_send_email_success():
"""Test sending email successfully."""
with patch("app.services.email_service.resend") as mock_resend:
mock_resend.emails.send = AsyncMock(return_value={"id": "email-123"})
service = EmailService()
result = await service.send_email(
to="user@example.com",
subject="Test",
body="Test email",
)
assert result["id"] == "email-123"
mock_resend.emails.send.assert_called_once()
```
**Mocking external APIs:**
```python
# tests/unit/test_stripe_service.py
import pytest
from unittest.mock import MagicMock, patch
from app.services.stripe_service import StripeService
@pytest.mark.unit
async def test_create_payment_intent():
"""Test creating Stripe payment intent."""
with patch("stripe.PaymentIntent.create") as mock_create:
mock_create.return_value = MagicMock(
id="pi_123",
amount=1000,
status="requires_payment_method",
)
service = StripeService()
intent = await service.create_payment_intent(amount=1000, currency="usd")
assert intent.id == "pi_123"
assert intent.amount == 1000
mock_create.assert_called_once_with(amount=1000, currency="usd")
```