Unit Testing & TDD Expert - Vitest/Jest
Test-Driven Development (TDD)
Red-Green-Refactor Cycle
// 1. RED: Write failing test describe('Calculator', () => { it('should add two numbers', () => { const calc = new Calculator(); expect(calc.add(2, 3)).toBe(5); // FAILS - doesn't exist }); });
// 2. GREEN: Minimal implementation class Calculator { add(a: number, b: number): number { return a + b; } }
// 3. REFACTOR: Improve design class Calculator { add(...numbers: number[]): number { return numbers.reduce((sum, n) => sum + n, 0); } }
TDD Checklist
-
RED: Test fails for the RIGHT reason
-
GREEN: Simplest code that passes
-
REFACTOR: All tests still green
Basic Test Structure
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('UserService', () => { let service: UserService;
beforeEach(() => { service = new UserService(); vi.clearAllMocks(); });
it('should create user', () => { const user = service.create({ name: 'John', email: 'john@test.com' });
expect(user).toMatchObject({
id: expect.any(String),
name: 'John',
email: 'john@test.com'
});
});
it('should throw for invalid email', () => { expect(() => service.create({ email: 'invalid' })).toThrow('Invalid email'); }); });
Mocking Strategies
- Mock Functions
const mockFn = vi.fn(); mockFn.mockReturnValue(42); expect(mockFn()).toBe(42); expect(mockFn).toHaveBeenCalledTimes(1);
- Mock Modules
vi.mock('./database', () => ({ query: vi.fn().mockResolvedValue([{ id: 1 }]) }));
import { query } from './database';
- ESM Mocking with vi.hoisted()
CRITICAL for ESM modules:
// ✅ Define mocks in hoisted context FIRST const { mockFn } = vi.hoisted(() => ({ mockFn: vi.fn() }));
vi.mock('./module', () => ({ myFunc: mockFn }));
import { myFunc } from './module';
- Dependency Injection
class UserService { constructor(private db: Database) {} }
// Test with mock const mockDb = { query: vi.fn().mockResolvedValue({ id: '123' }) }; const service = new UserService(mockDb);
Test Patterns
AAA Pattern (Arrange-Act-Assert)
it('should calculate total', () => { // Arrange const cart = new ShoppingCart(); cart.addItem({ price: 10, quantity: 2 });
// Act const total = cart.getTotal();
// Assert expect(total).toBe(20); });
Parametric Testing
describe.each([
[2, 3, 5],
[10, 5, 15],
[-1, 1, 0],
])('add(%i, %i)', (a, b, expected) => {
it(should return ${expected}, () => {
expect(add(a, b)).toBe(expected);
});
});
Coverage Analysis
// vitest.config.ts export default { test: { coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], lines: 80, functions: 80, branches: 80, } } };
Async Testing
it('should fetch user', async () => { const user = await api.fetchUser('123'); expect(user).toEqual({ id: '123', name: 'John' }); });
it('should handle API errors', async () => { await expect(api.fetchUser('invalid')).rejects.toThrow('Not found'); });
VSCode Debug Mode Fix
When tests spawn child processes, use clean environment:
function getCleanEnv(): NodeJS.ProcessEnv { const cleanEnv = { ...process.env }; delete cleanEnv.NODE_OPTIONS; delete cleanEnv.NODE_INSPECT; delete cleanEnv.VSCODE_INSPECTOR_OPTIONS; return cleanEnv; }
// Usage execSync('node cli.js', { env: getCleanEnv() });
Isolated Temp Directories
import * as os from 'os';
const TEST_ROOT = path.join(os.tmpdir(), test-${Date.now()});
beforeEach(() => fs.mkdir(TEST_ROOT, { recursive: true })); afterEach(() => fs.rm(TEST_ROOT, { recursive: true, force: true }));
Best Practices
-
Test behavior, not implementation
-
One assertion per test (when possible)
-
Independent tests (no shared state)
-
Fast tests (<100ms each)
-
Clear test names (should...)
-
Mock external dependencies only
Quick Reference
Assertions
expect(value).toBe(expected); // === expect(value).toEqual(expected); // Deep equality expect(array).toContain(item); // Array includes expect(string).toMatch(/pattern/); // Regex expect(fn).toThrow(Error); // Throws expect(obj).toHaveProperty('key'); // Has property
Mock Utilities
vi.fn() // Create mock vi.fn().mockReturnValue(x) // Return value vi.fn().mockResolvedValue(x) // Async return vi.mock('./module') // Mock module vi.spyOn(obj, 'method') // Spy on method vi.clearAllMocks() // Clear history
Related Skills
-
qa-engineer
-
Overall test strategy
-
e2e-testing
-
E2E and visual tests