Testing & Quality — Forge Skill
Overview
Tests exist to catch regressions and verify behavior. We value meaningful test coverage over 100% line coverage. Every test should have a clear reason to exist.
What Must Be Tested
Always Test (Non-Negotiable)
- Auth flows — login, logout, registration, password reset, token refresh
- Authorization logic — RLS policies, role-based access, permission checks
- Payment/billing logic — charges, subscriptions, refunds, webhooks
- Data mutations — create, update, delete operations
- API routes — request validation, response format, error handling, auth
- Critical user paths — core workflows that users depend on
- Complex business logic — algorithms, calculations, state machines
- Database migrations — verify data integrity after migration
Should Test (Expected)
- Utility functions — pure functions with multiple edge cases
- Custom hooks — complex hooks with state management
- Form validation — Zod schemas and validation logic
- Error handling — error boundaries, fallback behaviors
- Integration points — third-party API integrations
- Data transformations — serialization, normalization, formatting
Can Skip (Use Judgment)
- Simple getter/setter components with no logic
- Direct pass-through components
- Framework-provided functionality (Next.js routing, React rendering)
- Generated code (Supabase types, GraphQL codegen)
- Simple UI components that are just styling (unless they have interaction logic)
Test Quality Checks
Every Test Must
Test Naming Convention
// BAD
test('user', () => { ... });
test('test 1', () => { ... });
test('it works', () => { ... });
// GOOD
test('creates user with valid email and returns user object', () => { ... });
test('returns 401 when auth token is missing', () => { ... });
test('filters posts by category and sorts by date descending', () => { ... });
// Using describe blocks
describe('createUser', () => {
it('creates user with valid input', () => { ... });
it('throws ValidationError when email is invalid', () => { ... });
it('throws ConflictError when email already exists', () => { ... });
});
Assertion Quality
// BAD — weak assertions
expect(result).toBeDefined();
expect(result).not.toBeNull();
expect(users.length).toBeGreaterThan(0);
// GOOD — specific assertions
expect(result).toEqual({
id: expect.any(String),
email: 'user@example.com',
role: 'user',
});
expect(users).toHaveLength(3);
expect(users[0].name).toBe('Alice');
Avoid Test Anti-Patterns
| Anti-Pattern |
Problem |
Fix |
| Testing implementation details |
Brittle, breaks on refactor |
Test behavior and outputs |
| Excessive mocking |
Tests pass but code is broken |
Use integration tests for critical paths |
| Copy-paste test data |
Hard to maintain |
Use factories or fixtures |
test.skip without explanation |
Unknown if intentional |
Add comment or remove |
any in test files |
Hides type errors in test setup |
Type test data properly |
| Snapshot tests for logic |
Don't catch behavioral bugs |
Use explicit assertions |
| Testing third-party code |
Not our responsibility |
Mock at the boundary |
| Sleeping in tests |
Slow and flaky |
Use proper async utilities (waitFor, etc.) |
Test Organization
// Arrange-Act-Assert pattern
test('creates post and returns it with generated id', async () => {
// Arrange
const input = { title: 'Test Post', body: 'Content', authorId: userId };
// Act
const result = await createPost(input);
// Assert
expect(result.id).toEqual(expect.any(String));
expect(result.title).toBe('Test Post');
expect(result.authorId).toBe(userId);
});
Coverage Expectations
Coverage Targets
| Area |
Minimum Coverage |
| Auth & authorization |
90%+ |
| API routes |
85%+ |
| Business logic |
85%+ |
| Utility functions |
80%+ |
| UI components (with logic) |
70%+ |
| Overall project |
70%+ |
Coverage Is Not the Goal
- High coverage with weak assertions is worse than moderate coverage with strong assertions
- Don't write tests just to hit a coverage number
- Focus on behavioral coverage — are the important behaviors verified?
- Missing coverage in critical paths is more concerning than missing coverage in UI components
Test Types
Unit Tests (Vitest)
- Pure functions, utilities, transformations
- Isolated component logic (custom hooks)
- Fast, no external dependencies
Integration Tests (Vitest + Supabase)
- API routes with database
- Auth flows
- Service layer functions with real dependencies
E2E Tests (Playwright)
- Critical user journeys
- Cross-page flows
- Auth + protected routes
- Form submissions
Component Tests (React Testing Library)
- User interaction patterns
- Conditional rendering
- Form behavior
- Accessibility (role queries)
// GOOD — test from user's perspective
test('shows error message when form submitted with empty email', async () => {
render(<LoginForm />);
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
// BAD — testing implementation
test('sets error state when email is empty', () => {
const { result } = renderHook(() => useLoginForm());
act(() => result.current.submit());
expect(result.current.errors.email).toBe('required');
});
Review Severity
| Issue |
Severity |
| No tests for auth/authorization changes |
P0 — BLOCKED |
| No tests for payment/billing changes |
P0 — BLOCKED |
| No tests for API route changes |
P1 — High |
| Flaky test introduced |
P1 — High |
| Tests with weak/missing assertions |
P2 — Medium |
| Missing edge case coverage |
P2 — Medium |
| Test naming doesn't describe behavior |
P3 — Low |
| Test could be simplified |
P3 — Low |
1---2name: testing-quality3description: Testing & Quality — Forge Skill4---5# Testing & Quality — Forge Skill67## Overview89Tests exist to catch regressions and verify behavior. We value meaningful test coverage over 100% line coverage. Every test should have a clear reason to exist.1011## What Must Be Tested1213### Always Test (Non-Negotiable)1415- **Auth flows** — login, logout, registration, password reset, token refresh16- **Authorization logic** — RLS policies, role-based access, permission checks17- **Payment/billing logic** — charges, subscriptions, refunds, webhooks18- **Data mutations** — create, update, delete operations19- **API routes** — request validation, response format, error handling, auth20- **Critical user paths** — core workflows that users depend on21- **Complex business logic** — algorithms, calculations, state machines22- **Database migrations** — verify data integrity after migration2324### Should Test (Expected)2526- **Utility functions** — pure functions with multiple edge cases27- **Custom hooks** — complex hooks with state management28- **Form validation** — Zod schemas and validation logic29- **Error handling** — error boundaries, fallback behaviors30- **Integration points** — third-party API integrations31- **Data transformations** — serialization, normalization, formatting3233### Can Skip (Use Judgment)3435- Simple getter/setter components with no logic36- Direct pass-through components37- Framework-provided functionality (Next.js routing, React rendering)38- Generated code (Supabase types, GraphQL codegen)39- Simple UI components that are just styling (unless they have interaction logic)4041## Test Quality Checks4243### Every Test Must4445- [ ] Have a clear, descriptive name that explains the expected behavior46- [ ] Test one behavior per test case47- [ ] Be deterministic (no flaky tests)48- [ ] Be independent (no dependency on other tests or test order)49- [ ] Clean up after itself (no side effects on other tests)50- [ ] Assert the right thing (not just "doesn't crash")5152### Test Naming Convention5354```typescript55// BAD56test('user', () => { ... });57test('test 1', () => { ... });58test('it works', () => { ... });5960// GOOD61test('creates user with valid email and returns user object', () => { ... });62test('returns 401 when auth token is missing', () => { ... });63test('filters posts by category and sorts by date descending', () => { ... });6465// Using describe blocks66describe('createUser', () => {67 it('creates user with valid input', () => { ... });68 it('throws ValidationError when email is invalid', () => { ... });69 it('throws ConflictError when email already exists', () => { ... });70});71```7273### Assertion Quality7475```typescript76// BAD — weak assertions77expect(result).toBeDefined();78expect(result).not.toBeNull();79expect(users.length).toBeGreaterThan(0);8081// GOOD — specific assertions82expect(result).toEqual({83 id: expect.any(String),84 email: 'user@example.com',85 role: 'user',86});87expect(users).toHaveLength(3);88expect(users[0].name).toBe('Alice');89```9091### Avoid Test Anti-Patterns9293| Anti-Pattern | Problem | Fix |94|-------------|---------|-----|95| Testing implementation details | Brittle, breaks on refactor | Test behavior and outputs |96| Excessive mocking | Tests pass but code is broken | Use integration tests for critical paths |97| Copy-paste test data | Hard to maintain | Use factories or fixtures |98| `test.skip` without explanation | Unknown if intentional | Add comment or remove |99| `any` in test files | Hides type errors in test setup | Type test data properly |100| Snapshot tests for logic | Don't catch behavioral bugs | Use explicit assertions |101| Testing third-party code | Not our responsibility | Mock at the boundary |102| Sleeping in tests | Slow and flaky | Use proper async utilities (waitFor, etc.) |103104### Test Organization105106```typescript107// Arrange-Act-Assert pattern108test('creates post and returns it with generated id', async () => {109 // Arrange110 const input = { title: 'Test Post', body: 'Content', authorId: userId };111112 // Act113 const result = await createPost(input);114115 // Assert116 expect(result.id).toEqual(expect.any(String));117 expect(result.title).toBe('Test Post');118 expect(result.authorId).toBe(userId);119});120```121122## Coverage Expectations123124### Coverage Targets125126| Area | Minimum Coverage |127|------|-----------------|128| Auth & authorization | 90%+ |129| API routes | 85%+ |130| Business logic | 85%+ |131| Utility functions | 80%+ |132| UI components (with logic) | 70%+ |133| Overall project | 70%+ |134135### Coverage Is Not the Goal136137- High coverage with weak assertions is worse than moderate coverage with strong assertions138- Don't write tests just to hit a coverage number139- Focus on **behavioral coverage** — are the important behaviors verified?140- Missing coverage in critical paths is more concerning than missing coverage in UI components141142## Test Types143144### Unit Tests (Vitest)145146- Pure functions, utilities, transformations147- Isolated component logic (custom hooks)148- Fast, no external dependencies149150### Integration Tests (Vitest + Supabase)151152- API routes with database153- Auth flows154- Service layer functions with real dependencies155156### E2E Tests (Playwright)157158- Critical user journeys159- Cross-page flows160- Auth + protected routes161- Form submissions162163### Component Tests (React Testing Library)164165- User interaction patterns166- Conditional rendering167- Form behavior168- Accessibility (role queries)169170```typescript171// GOOD — test from user's perspective172test('shows error message when form submitted with empty email', async () => {173 render(<LoginForm />);174175 await userEvent.click(screen.getByRole('button', { name: /sign in/i }));176177 expect(screen.getByText(/email is required/i)).toBeInTheDocument();178});179180// BAD — testing implementation181test('sets error state when email is empty', () => {182 const { result } = renderHook(() => useLoginForm());183 act(() => result.current.submit());184 expect(result.current.errors.email).toBe('required');185});186```187188## Review Severity189190| Issue | Severity |191|-------|----------|192| No tests for auth/authorization changes | P0 — BLOCKED |193| No tests for payment/billing changes | P0 — BLOCKED |194| No tests for API route changes | P1 — High |195| Flaky test introduced | P1 — High |196| Tests with weak/missing assertions | P2 — Medium |197| Missing edge case coverage | P2 — Medium |198| Test naming doesn't describe behavior | P3 — Low |199| Test could be simplified | P3 — Low |