Testing Guide
Best practices for testing in the platform monorepo.
Commands
pnpm test # All tests
pnpm test:coverage # With coverage
pnpx vitest path/to/file.test.ts # Specific file
pnpx vitest --watch # Watch modeTools
| Tool | Purpose |
|---|---|
| Vitest | Unit tests |
| React Testing Library | Component tests |
| Playwright | End-to-end tests |
Test File Location
Co-locate test files with the code they test:
packages/core/src/
├── application/
│ └── use-cases/
│ └── transaction/
│ ├── create-transaction.ts
│ └── create-transaction.test.ts
├── infrastructure/
│ └── repositories/
│ ├── transaction-repository.ts
│ └── transaction-repository.test.tsTest Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('@repo/core/infrastructure/repositories/engagement-repository');
describe('CreateEngagement', () => {
beforeEach(() => vi.clearAllMocks());
it('creates engagement with valid input', async () => {
// Arrange
vi.mocked(EngagementRepository.create).mockResolvedValue(mockEngagement);
// Act
const result = await new CreateEngagement().execute(input);
// Assert
expect(result.engagement).toEqual(mockEngagement);
});
it('throws NotFoundError when org not found', async () => {
vi.mocked(OrgRepository.findById).mockResolvedValue(null);
await expect(useCase.execute(input)).rejects.toThrow(NotFoundError);
});
});What to Test
| Test Type | Examples |
|---|---|
| User interactions | Click handlers, form submissions, keyboard navigation |
| Conditional rendering | Loading states, error states, empty states |
| Data transformations | Formatting functions, filters, calculations |
| Accessibility | ARIA attributes, keyboard support, screen reader text |
What NOT to Test
- Implementation details - Avoid testing internal state, private methods, or component internals
- Third-party libraries - Trust that React Query, Zustand, etc. work correctly
- Trivial code - Simple pass-through components or obvious logic don't need tests
- Over-mocking - Over-mocking leads to tests that pass but don't catch real bugs
- Test IDs first - Prefer accessible queries:
getByRole,getByLabelText,getByText
Snapshot Tests
Use snapshots only for:
- Serialized data structures - API responses, configuration objects, GraphQL queries
- Generated output - Email templates, exported data formats, structured logs
Do not use snapshots for:
- UI components (too brittle, changes are hard to review meaningfully)
- Large objects (diffs become unreadable)
Diagnosing Failures
| Cause | Symptom | Fix |
|---|---|---|
| Mock not set up | "undefined" errors | Add vi.mock() |
| Missing await | Promise comparison fails | Add await |
| State pollution | Intermittent failures | Add beforeEach cleanup |
| Type mismatch | Mock doesn't match | Update mock shape |
| Logic changed | Assertion fails | Update test expectation |
Fixing Tests
// Implementation changed - update expectation
expect(result.status).toBe('draft'); // was 'pending'
// New field added - update mock
vi.mocked(Repo.findById).mockResolvedValue({
id: '123',
name: 'Test',
createdAt: new Date(), // new field
});
// New validation - add test
it('throws ValidationError for invalid email', async () => {
await expect(useCase.execute({ email: 'bad' })).rejects.toThrow(ValidationError);
});Code Coverage
Thresholds by Package
| Package Type | Threshold | Rationale |
|---|---|---|
packages/core | 80%+ | Critical business logic |
packages/luz | 60-70% | UI components are harder to unit test |
apps/web | 50-60% | Presentation layer has less testable logic |
What to Measure
Focus coverage on:
- Use cases (
packages/core/src/application/) - Repositories (
packages/core/src/infrastructure/) - Utility functions and transformations
Exclude from coverage:
- Generated types (GraphQL codegen)
- Barrel files (
index.tsre-exports) - Configuration files
- Third-party wrapper code
Configuration Example
// packages/core/vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/**/*.stories.tsx', 'src/index.ts'],
},
},
});Best Practices
- Arrange-Act-Assert - clear test structure
- Descriptive names -
it('returns null when not found') - One assertion per test - focused tests
- Mock only dependencies - not implementation details
- Clear mocks in beforeEach - prevent pollution
Key Principle
Coverage is a tool, not a target. High coverage on trivial code is less valuable than moderate coverage on critical business logic. Focus testing effort on:
- Use cases with business rules
- Data transformations
- Error handling paths
- Edge cases in critical flows
Related Files
| Type | Location |
|---|---|
| Vitest Config | vitest.config.ts |
| Core Tests | packages/core/src/**/*.test.ts |
| E2E Tests | apps/web/e2e/ |