Testing Guide
This template includes comprehensive testing setup with Vitest for unit/integration tests and Playwright for E2E tests.
Unit & Integration Testing
Running Tests
# Run all tests once
pnpm run test
# Watch mode (re-run on file changes)
pnpm run test:watch
# Run with coverage
pnpm run test:coverage
# Run specific test file
pnpm run test src/core.test.ts
# Run tests matching pattern
pnpm run test -- --grep "string utility"
# Interactive UI
pnpm run test:ui
Writing Tests
Create a test file next to your source:
// src/string-utils.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// src/string-utils.test.ts
import { describe, it, expect } from 'vitest';
import { capitalize } from './string-utils';
describe('String Utils', () => {
describe('capitalize', () => {
it('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
it('should handle empty strings', () => {
expect(capitalize('')).toBe('');
});
});
});
Coverage Thresholds
Current configuration requires:
- 80% line coverage
- 80% function coverage
- 75% branch coverage
- 80% statement coverage
Configure in vitest.config.ts.
Using Test Utilities
The @company/test-utils package provides mocks and fixtures:
import { describe, it, expect } from 'vitest';
import {
createMockUser,
createMockApiResponse,
createMockFn,
spyOn,
} from '@company/test-utils';
describe('User API', () => {
it('should fetch user', async () => {
const mockUser = createMockUser({ name: 'John' });
const mockFn = createMockFn();
mockFn.mockResolvedValue(mockUser);
const result = await mockFn();
expect(result.name).toBe('John');
});
it('should spy on console', () => {
const consoleSpy = spyOn(console, 'log');
console.log('test');
expect(consoleSpy).toHaveBeenCalledWith('test');
});
});
Cross-Package Testing
Test integration between packages in integration.test.ts:
import { describe, it, expect } from 'vitest';
import { isValidEmail, createSuccessResponse } from '@company/core';
import { capitalize } from '@company/utils';
describe('Cross-Package Integration', () => {
it('should combine utilities', () => {
const response = createSuccessResponse({
email: 'john@example.com',
isValid: isValidEmail('john@example.com'),
displayName: capitalize('john'),
});
expect(response.success).toBe(true);
expect(response.data?.isValid).toBe(true);
});
});
Performance Benchmarking
Running Benchmarks
# Run all benchmarks
pnpm run test vitest.benchmark.config.ts
# Run specific benchmark
pnpm run test -- bench "Array Operations"
Writing Benchmarks
Create benchmark files (usually separate from unit tests):
// vitest.benchmark.config.ts
import { bench, describe } from 'vitest';
import { unique, flatten } from '@company/utils';
describe('Benchmarks', () => {
const largeArray = Array.from({ length: 10000 }, (_, i) => i % 100);
bench('unique - large array', () => {
unique(largeArray);
});
bench('flatten - deep array', () => {
flatten(Array(100).fill([1, [2, [3]]]), 2);
});
});
End-to-End Testing
Running E2E Tests
# Run all E2E tests
pnpm exec playwright test
# Run in headed mode (see browser)
pnpm exec playwright test --headed
# Run specific file
pnpm exec playwright test e2e/example.spec.ts
# Run in debug mode
pnpm exec playwright test --debug
# View test report
pnpm exec playwright show-report
Writing E2E Tests
Create test files in e2e/ directory:
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should login successfully', async ({ page }) => {
// Fill login form
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
// Submit form
await page.click('button[type="submit"]');
// Wait for redirect and verify
await page.waitForURL('/dashboard');
expect(page.url()).toContain('/dashboard');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'wrong');
await page.click('button[type="submit"]');
const errorMessage = page.locator('[role="alert"]');
await expect(errorMessage).toContainText('Invalid credentials');
});
});
Multi-Browser Testing
Tests run against Chromium, Firefox, and WebKit by default. Configure in playwright.config.ts.
Visual Regression Testing
test('should render correctly', async ({ page }) => {
await page.goto('/');
// Take screenshot
await expect(page).toHaveScreenshot();
});
Run with --update-snapshots to create baseline screenshots.
Testing Best Practices
1. Test Naming
✅ Good: Describes what should happen
it('should return capitalized string when input is lowercase', () => {})
❌ Bad: Vague or implementation-focused
it('test capitalize function', () => {})
2. Arrange-Act-Assert Pattern
it('should process user data', () => {
// Arrange: Set up test data
const user = createMockUser({ name: 'John' });
// Act: Call the function
const result = processUser(user);
// Assert: Verify the result
expect(result.displayName).toBe('John');
});
3. Avoid Test Interdependence
❌ Bad: Tests depend on execution order
let user;
test('create user', () => {
user = createUser({ name: 'John' });
});
test('update user', () => {
updateUser(user);
});
✅ Good: Each test is independent
test('can create user', () => {
const user = createUser({ name: 'John' });
expect(user.name).toBe('John');
});
test('can update user', () => {
const user = createMockUser();
const updated = updateUser(user);
expect(updated.name).toBe('John');
});
4. Use Fixtures for Common Setup
// test-utils/fixtures.ts
export function createMockUser(overrides?: Partial<User>): User {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides,
};
}
// In tests
const user = createMockUser({ name: 'John' });
5. Test Behavior, Not Implementation
✅ Good: Testing what the function does
it('should return unique items', () => {
expect(unique([1, 1, 2, 3])).toEqual([1, 2, 3]);
});
❌ Bad: Testing how it's implemented
it('should create a Set internally', () => {
// Don't test internal implementation
});
Coverage Reports
Generate and view coverage reports:
# Generate coverage
pnpm run test:coverage
# View HTML report
open coverage/index.html
Coverage is automatically tracked and must meet thresholds before passing CI.
CI/CD Integration
Tests run automatically on:
- Pre-commit: Type checking and linting
- Push: All tests, coverage, and build validation
- Pull Request: Same checks plus coverage reports
See .github/workflows/ci.yml for details.
Troubleshooting
"Module not found" in tests
# Rebuild packages
pnpm run build
# Clear Vitest cache
rm -rf node_modules/.vitest
Tests timeout
// Increase timeout for slow tests
it('slow test', async () => {
// ...
}, { timeout: 10000 });
Coverage not showing accurate results
# Clear coverage cache and regenerate
rm -rf coverage
pnpm run test:coverage