[ PROMPT_NODE_24921 ]
Resources – Implementation Playbook
[ SKILL_DOCUMENTATION ]
# JavaScript Testing Patterns Implementation Playbook
This file contains detailed patterns, checklists, and code samples referenced by the skill.
# JavaScript Testing Patterns
Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.
## When to Use This Skill
- Setting up test infrastructure for new projects
- Writing unit tests for functions and classes
- Creating integration tests for APIs and services
- Implementing end-to-end tests for user flows
- Mocking external dependencies and APIs
- Testing React, Vue, or other frontend components
- Implementing test-driven development (TDD)
- Setting up continuous testing in CI/CD pipelines
## Testing Frameworks
### Jest - Full-Featured Testing Framework
**Setup:**
```typescript
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.interface.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
setupFilesAfterEnv: ['/src/test/setup.ts'],
};
export default config;
```
### Vitest - Fast, Vite-Native Testing
**Setup:**
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/*.d.ts', '**/*.config.ts', '**/dist/**'],
},
setupFiles: ['./src/test/setup.ts'],
},
});
```
## Unit Testing Patterns
### Pattern 1: Testing Pure Functions
```typescript
// utils/calculator.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// utils/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { add, divide } from './calculator';
describe('Calculator', () => {
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
it('should handle zero', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
});
});
describe('divide', () => {
it('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should handle decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
it('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
```
### Pattern 2: Testing Classes
```typescript
// services/user.service.ts
export class UserService {
private users: Map = new Map();
create(user: User): User {
if (this.users.has(user.id)) {
throw new Error('User already exists');
}
this.users.set(user.id, user);
return user;
}
findById(id: string): User | undefined {
return this.users.get(id);
}
update(id: string, updates: Partial): User {
const user = this.users.get(id);
if (!user) {
throw new Error('User not found');
}
const updated = { ...user, ...updates };
this.users.set(id, updated);
return updated;
}
delete(id: string): boolean {
return this.users.delete(id);
}
}
// services/user.service.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
describe('create', () => {
it('should create a new user', () => {
const user = { id: '1', name: 'John', email: '[email protected]' };
const created = service.create(user);
expect(created).toEqual(user);
expect(service.findById('1')).toEqual(user);
});
it('should throw error if user already exists', () => {
const user = { id: '1', name: 'John', email: '[email protected]' };
service.create(user);
expect(() => service.create(user)).toThrow('User already exists');
});
});
describe('update', () => {
it('should update existing user', () => {
const user = { id: '1', name: 'John', email: '[email protected]' };
service.create(user);
const updated = service.update('1', { name: 'Jane' });
expect(updated.name).toBe('Jane');
expect(updated.email).toBe('[email protected]');
});
it('should throw error if user not found', () => {
expect(() => service.update('999', { name: 'Jane' }))
.toThrow('User not found');
});
});
});
```
### Pattern 3: Testing Async Functions
```typescript
// services/api.service.ts
export class ApiService {
async fetchUser(id: string): Promise {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
async createUser(user: CreateUserDTO): Promise {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
});
return response.json();
}
}
// services/api.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApiService } from './api.service';
// Mock fetch globally
global.fetch = vi.fn();
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
service = new ApiService();
vi.clearAllMocks();
});
describe('fetchUser', () => {
it('should fetch user successfully', async () => {
const mockUser = { id: '1', name: 'John', email: '[email protected]' };
(fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const user = await service.fetchUser('1');
expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
it('should throw error if user not found', async () => {
(fetch as any).mockResolvedValueOnce({
ok: false,
});
await expect(service.fetchUser('999')).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
it('should create user successfully', async () => {
const newUser = { name: 'John', email: '[email protected]' };
const createdUser = { id: '1', ...newUser };
(fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => createdUser,
});
const user = await service.createUser(newUser);
expect(user).toEqual(createdUser);
expect(fetch).toHaveBeenCalledWith(
'https://api.example.com/users',
expect.objectContaining({
method: 'POST',
body: JSON.stringify(newUser),
})
);
});
});
});
```
## Mocking Patterns
### Pattern 1: Mocking Modules
```typescript
// services/email.service.ts
import nodemailer from 'nodemailer';
export class EmailService {
private transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
async sendEmail(to: string, subject: string, html: string) {
await this.transporter.sendMail({
from: process.env.EMAIL_FROM,
to,
subject,
html,
});
}
}
// services/email.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EmailService } from './email.service';
vi.mock('nodemailer', () => ({
default: {
createTransport: vi.fn(() => ({
sendMail: vi.fn().mockResolvedValue({ messageId: '123' }),
})),
},
}));
describe('EmailService', () => {
let service: EmailService;
beforeEach(() => {
service = new EmailService();
});
it('should send email successfully', async () => {
await service.sendEmail(
'[email protected]',
'Test Subject',
'
Test Body
' ); expect(service['transporter'].sendMail).toHaveBeenCalledWith( expect.objectContaining({ to: '[email protected]', subject: 'Test Subject', }) ); }); }); ``` ### Pattern 2: Dependency Injection for Testing ```typescript // services/user.service.ts export interface IUserRepository { findById(id: string): Promise; create(user: User): Promise; } export class UserService { constructor(private userRepository: IUserRepository) {} async getUser(id: string): Promise { const user = await this.userRepository.findById(id); if (!user) { throw new Error('User not found'); } return user; } async createUser(userData: CreateUserDTO): Promise { // Business logic here const user = { id: generateId(), ...userData }; return this.userRepository.create(user); } } // services/user.service.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { UserService, IUserRepository } from './user.service'; describe('UserService', () => { let service: UserService; let mockRepository: IUserRepository; beforeEach(() => { mockRepository = { findById: vi.fn(), create: vi.fn(), }; service = new UserService(mockRepository); }); describe('getUser', () => { it('should return user if found', async () => { const mockUser = { id: '1', name: 'John', email: '[email protected]' }; vi.mocked(mockRepository.findById).mockResolvedValue(mockUser); const user = await service.getUser('1'); expect(user).toEqual(mockUser); expect(mockRepository.findById).toHaveBeenCalledWith('1'); }); it('should throw error if user not found', async () => { vi.mocked(mockRepository.findById).mockResolvedValue(null); await expect(service.getUser('999')).rejects.toThrow('User not found'); }); }); describe('createUser', () => { it('should create user successfully', async () => { const userData = { name: 'John', email: '[email protected]' }; const createdUser = { id: '1', ...userData }; vi.mocked(mockRepository.create).mockResolvedValue(createdUser); const user = await service.createUser(userData); expect(user).toEqual(createdUser); expect(mockRepository.create).toHaveBeenCalled(); }); }); }); ``` ### Pattern 3: Spying on Functions ```typescript // utils/logger.ts export const logger = { info: (message: string) => console.log(`INFO: ${message}`), error: (message: string) => console.error(`ERROR: ${message}`), }; // services/order.service.ts import { logger } from '../utils/logger'; export class OrderService { async processOrder(orderId: string): Promise { logger.info(`Processing order ${orderId}`); // Process order logic logger.info(`Order ${orderId} processed successfully`); } } // services/order.service.test.ts import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { OrderService } from './order.service'; import { logger } from '../utils/logger'; describe('OrderService', () => { let service: OrderService; let loggerSpy: any; beforeEach(() => { service = new OrderService(); loggerSpy = vi.spyOn(logger, 'info'); }); afterEach(() => { loggerSpy.mockRestore(); }); it('should log order processing', async () => { await service.processOrder('123'); expect(loggerSpy).toHaveBeenCalledWith('Processing order 123'); expect(loggerSpy).toHaveBeenCalledWith('Order 123 processed successfully'); expect(loggerSpy).toHaveBeenCalledTimes(2); }); }); ``` ## Integration Testing ### Pattern 1: API Integration Tests ```typescript // tests/integration/user.api.test.ts import request from 'supertest'; import { app } from '../../src/app'; import { pool } from '../../src/config/database'; describe('User API Integration Tests', () => { beforeAll(async () => { // Setup test database await pool.query('CREATE TABLE IF NOT EXISTS users (...)'); }); afterAll(async () => { // Cleanup await pool.query('DROP TABLE IF EXISTS users'); await pool.end(); }); beforeEach(async () => { // Clear data before each test await pool.query('TRUNCATE TABLE users CASCADE'); }); describe('POST /api/users', () => { it('should create a new user', async () => { const userData = { name: 'John Doe', email: '[email protected]', password: 'password123', }; const response = await request(app) .post('/api/users') .send(userData) .expect(201); expect(response.body).toMatchObject({ name: userData.name, email: userData.email, }); expect(response.body).toHaveProperty('id'); expect(response.body).not.toHaveProperty('password'); }); it('should return 400 if email is invalid', async () => { const userData = { name: 'John Doe', email: 'invalid-email', password: 'password123', }; const response = await request(app) .post('/api/users') .send(userData) .expect(400); expect(response.body).toHaveProperty('error'); }); it('should return 409 if email already exists', async () => { const userData = { name: 'John Doe', email: '[email protected]', password: 'password123', }; await request(app).post('/api/users').send(userData); const response = await request(app) .post('/api/users') .send(userData) .expect(409); expect(response.body.error).toContain('already exists'); }); }); describe('GET /api/users/:id', () => { it('should get user by id', async () => { const createResponse = await request(app) .post('/api/users') .send({ name: 'John Doe', email: '[email protected]', password: 'password123', }); const userId = createResponse.body.id; const response = await request(app) .get(`/api/users/${userId}`) .expect(200); expect(response.body).toMatchObject({ id: userId, name: 'John Doe', email: '[email protected]', }); }); it('should return 404 if user not found', async () => { await request(app) .get('/api/users/999') .expect(404); }); }); describe('Authentication', () => { it('should require authentication for protected routes', async () => { await request(app) .get('/api/users/me') .expect(401); }); it('should allow access with valid token', async () => { // Create user and login await request(app) .post('/api/users') .send({ name: 'John Doe', email: '[email protected]', password: 'password123', }); const loginResponse = await request(app) .post('/api/auth/login') .send({ email: '[email protected]', password: 'password123', }); const token = loginResponse.body.token; const response = await request(app) .get('/api/users/me') .set('Authorization', `Bearer ${token}`) .expect(200); expect(response.body.email).toBe('[email protected]'); }); }); }); ``` ### Pattern 2: Database Integration Tests ```typescript // tests/integration/user.repository.test.ts import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { Pool } from 'pg'; import { UserRepository } from '../../src/repositories/user.repository'; describe('UserRepository Integration Tests', () => { let pool: Pool; let repository: UserRepository; beforeAll(async () => { pool = new Pool({ host: 'localhost', port: 5432, database: 'test_db', user: 'test_user', password: 'test_password', }); repository = new UserRepository(pool); // Create tables await pool.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); }); afterAll(async () => { await pool.query('DROP TABLE IF EXISTS users'); await pool.end(); }); beforeEach(async () => { await pool.query('TRUNCATE TABLE users CASCADE'); }); it('should create a user', async () => { const user = await repository.create({ name: 'John Doe', email: '[email protected]', password: 'hashed_password', }); expect(user).toHaveProperty('id'); expect(user.name).toBe('John Doe'); expect(user.email).toBe('[email protected]'); }); it('should find user by email', async () => { await repository.create({ name: 'John Doe', email: '[email protected]', password: 'hashed_password', }); const user = await repository.findByEmail('[email protected]'); expect(user).toBeTruthy(); expect(user?.name).toBe('John Doe'); }); it('should return null if user not found', async () => { const user = await repository.findByEmail('[email protected]'); expect(user).toBeNull(); }); }); ``` ## Frontend Testing with Testing Library ### Pattern 1: React Component Testing ```typescript // components/UserForm.tsx import { useState } from 'react'; interface Props { onSubmit: (user: { name: string; email: string }) => void; } export function UserForm({ onSubmit }: Props) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit({ name, email }); }; return ( setName(e.target.value)} data-testid="name-input" /> setEmail(e.target.value)} data-testid="email-input" /> ); } // components/UserForm.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { UserForm } from './UserForm'; describe('UserForm', () => { it('should render form inputs', () => { render(); expect(screen.getByPlaceholderText('Name')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Email')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); }); it('should update input values', () => { render(); const nameInput = screen.getByTestId('name-input') as HTMLInputElement; const emailInput = screen.getByTestId('email-input') as HTMLInputElement; fireEvent.change(nameInput, { target: { value: 'John Doe' } }); fireEvent.change(emailInput, { target: { value: '[email protected]' } }); expect(nameInput.value).toBe('John Doe'); expect(emailInput.value).toBe('[email protected]'); }); it('should call onSubmit with form data', () => { const onSubmit = vi.fn(); render(); fireEvent.change(screen.getByTestId('name-input'), { target: { value: 'John Doe' }, }); fireEvent.change(screen.getByTestId('email-input'), { target: { value: '[email protected]' }, }); fireEvent.click(screen.getByRole('button', { name: 'Submit' })); expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe', email: '[email protected]', }); }); }); ``` ### Pattern 2: Testing Hooks ```typescript // hooks/useCounter.ts import { useState, useCallback } from 'react'; export function useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue); const increment = useCallback(() => setCount((c) => c + 1), []); const decrement = useCallback(() => setCount((c) => c - 1), []); const reset = useCallback(() => setCount(initialValue), [initialValue]); return { count, increment, decrement, reset }; } // hooks/useCounter.test.ts import { renderHook, act } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { useCounter } from './useCounter'; describe('useCounter', () => { it('should initialize with default value', () => { const { result } = renderHook(() => useCounter()); expect(result.current.count).toBe(0); }); it('should initialize with custom value', () => { const { result } = renderHook(() => useCounter(10)); expect(result.current.count).toBe(10); }); it('should increment count', () => { const { result } = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it('should decrement count', () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(4); }); it('should reset to initial value', () => { const { result } = renderHook(() => useCounter(10)); act(() => { result.current.increment(); result.current.increment(); }); expect(result.current.count).toBe(12); act(() => { result.current.reset(); }); expect(result.current.count).toBe(10); }); }); ``` ## Test Fixtures and Factories ```typescript // tests/fixtures/user.fixture.ts import { faker } from '@faker-js/faker'; export function createUserFixture(overrides?: Partial): User { return { id: faker.string.uuid(), name: faker.person.fullName(), email: faker.internet.email(), createdAt: faker.date.past(), ...overrides, }; } export function createUsersFixture(count: number): User[] { return Array.from({ length: count }, () => createUserFixture()); } // Usage in tests import { createUserFixture, createUsersFixture } from '../fixtures/user.fixture'; describe('UserService', () => { it('should process user', () => { const user = createUserFixture({ name: 'John Doe' }); // Use user in test }); it('should handle multiple users', () => { const users = createUsersFixture(10); // Use users in test }); }); ``` ## Snapshot Testing ```typescript // components/UserCard.test.tsx import { render } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { UserCard } from './UserCard'; describe('UserCard', () => { it('should match snapshot', () => { const user = { id: '1', name: 'John Doe', email: '[email protected]', avatar: 'https://example.com/avatar.jpg', }; const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); it('should match snapshot with loading state', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); }); ``` ## Coverage Reports ```typescript // package.json { "scripts": { "test": "vitest", "test:coverage": "vitest --coverage", "test:ui": "vitest --ui" } } ``` ## Best Practices 1. **Follow AAA Pattern**: Arrange, Act, Assert 2. **One assertion per test**: Or logically related assertions 3. **Descriptive test names**: Should describe what is being tested 4. **Use beforeEach/afterEach**: For setup and teardown 5. **Mock external dependencies**: Keep tests isolated 6. **Test edge cases**: Not just happy paths 7. **Avoid implementation details**: Test behavior, not implementation 8. **Use test factories**: For consistent test data 9. **Keep tests fast**: Mock slow operations 10. **Write tests first (TDD)**: When possible 11. **Maintain test coverage**: Aim for 80%+ coverage 12. **Use TypeScript**: For type-safe tests 13. **Test error handling**: Not just success cases 14. **Use data-testid sparingly**: Prefer semantic queries 15. **Clean up after tests**: Prevent test pollution ## Common Patterns ### Test Organization ```typescript describe('UserService', () => { describe('createUser', () => { it('should create user successfully', () => {}); it('should throw error if email exists', () => {}); it('should hash password', () => {}); }); describe('updateUser', () => { it('should update user', () => {}); it('should throw error if not found', () => {}); }); }); ``` ### Testing Promises ```typescript // Using async/await it('should fetch user', async () => { const user = await service.fetchUser('1'); expect(user).toBeDefined(); }); // Testing rejections it('should throw error', async () => { await expect(service.fetchUser('invalid')).rejects.toThrow('Not found'); }); ``` ### Testing Timers ```typescript import { vi } from 'vitest'; it('should call function after delay', () => { vi.useFakeTimers(); const callback = vi.fn(); setTimeout(callback, 1000); expect(callback).not.toHaveBeenCalled(); vi.advanceTimersByTime(1000); expect(callback).toHaveBeenCalled(); vi.useRealTimers(); }); ``` ## Resources - **Jest Documentation**: https://jestjs.io/ - **Vitest Documentation**: https://vitest.dev/ - **Testing Library**: https://testing-library.com/ - **Kent C. Dodds Testing Blog**: https://kentcdodds.com/blog/
Source: claude-code-templates (MIT). See About Us for full credits.