TDD Workflow
Enforce test-driven development: write tests first, implement to pass, then refactor.
RED: Write Failing Tests
Define expected behavior before writing any implementation:
describe('searchMarkets', () => {
it('returns relevant markets for query', async () => {
const results = await searchMarkets('election')
expect(results).toHaveLength(5)
expect(results[0].relevanceScore).toBeGreaterThan(0.8)
})
it('returns empty array for no matches', async () => {
const results = await searchMarkets('zzz_nonexistent')
expect(results).toEqual([])
})
it('handles empty query gracefully', async () => {
const results = await searchMarkets('')
expect(results).toEqual([])
})
})
Run tests -- they MUST fail:
npm test -- --run [test-file]
GREEN: Write Minimal Implementation
Write just enough code to make all tests pass. No more.
npm test -- --run [test-file]
# All tests should now pass
REFACTOR: Improve While Green
Improve code quality with tests as safety net:
- Remove duplication
- Improve naming
- Optimize performance
- Extract helpers
Run tests after each change to confirm nothing broke.
| Type | What to Test | Tool | Speed Target |
|---|---|---|---|
| Unit | Single function/class, pure logic | Vitest/Jest | < 50ms each |
| Integration | API endpoints, DB operations, service interactions | Vitest + mocks | < 500ms each |
| E2E | Critical user flows through browser | Playwright | < 30s each |
IMPORTANT: curl tests are integration tests, NOT E2E. E2E requires browser verification.
Target 80%+ across all metrics:
npm run test:coverage
Check branches, functions, lines, and statements individually.
Test behavior, not implementation
- WRONG:
expect(component.state.count).toBe(5) - RIGHT:
expect(screen.getByText('Count: 5')).toBeInTheDocument()
- WRONG:
Each test is independent -- set up own data, no shared mutable state
Use semantic selectors
- WRONG:
page.click('.css-xyz') - RIGHT:
page.click('[data-testid="submit"]')orpage.click('button:has-text("Submit")')
- WRONG:
Mock external dependencies -- isolate the unit under test
Test edge cases -- null, undefined, empty, boundary values, error paths
Arrange-Act-Assert structure in every test
Database/ORM
vi.mock('@/lib/prisma', () => ({
prisma: { user: { findUnique: vi.fn() } }
}))
External APIs
vi.mock('@/lib/external-api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mocked' }))
}))
- If tests pass immediately (no RED phase): the test is not testing anything meaningful. Add assertions that verify specific behavior.
- If coverage is below 80%: run
npm run test:coverageand check the uncovered lines report. Add tests for missed branches. - If E2E tests are flaky: replace
waitForTimeoutwithwaitForSelectororexpect().toBeVisible(). Never use fixed timeouts. - If mocks leak between tests: add
vi.restoreAllMocks()inafterEachor usevi.mockat module level. - If tests are slow (unit > 50ms): check for unmocked network calls or missing test isolation.