unit-testing
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
1. Mock Functions
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn).toHaveBeenCalledTimes(1);
2. Mock Modules
vi.mock('./database', () => ({
query: vi.fn().mockResolvedValue([{ id: 1 }])
}));
import { query } from './database';
3. 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';
4. 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 strategye2e-testing- E2E and visual tests
More from anton-abyzov/specweave
technical-writing
Technical writing expert for API documentation, README files, tutorials, changelog management, and developer documentation. Covers style guides, information architecture, versioning docs, OpenAPI/Swagger, and documentation-as-code. Activates for technical writing, API docs, README, changelog, tutorial writing, documentation, technical communication, style guide, OpenAPI, Swagger, developer docs.
45spec-driven-brainstorming
Spec-driven brainstorming and product discovery expert. Helps teams ideate features, break down epics, conduct story mapping sessions, prioritize using MoSCoW/RICE/Kano, and validate ideas with lean startup methods. Activates for brainstorming, product discovery, story mapping, feature ideation, prioritization, MoSCoW, RICE, Kano model, lean startup, MVP definition, product backlog, feature breakdown.
43kafka-architecture
Apache Kafka architecture expert for cluster design, capacity planning, and high availability. Use when designing Kafka clusters, choosing partition strategies, or sizing brokers for production workloads.
34docusaurus
Docusaurus 3.x documentation framework - MDX authoring, theming, versioning, i18n. Use for documentation sites or spec-weave.com.
29frontend
Expert frontend developer for React, Vue, Angular, and modern JavaScript/TypeScript. Use when creating components, implementing hooks, handling state management, or building responsive web interfaces. Covers React 18+ features, custom hooks, form handling, and accessibility best practices.
29reflect
Self-improving AI memory system that persists learnings across sessions in CLAUDE.md. Use when capturing corrections, remembering user preferences, or extracting patterns from successful implementations. Enables continual learning without starting from zero each conversation.
27