JavaScript Testing Patterns
Comprehensive testing strategies for JavaScript/TypeScript applications.
Framework Setup
Jest Configuration
// jest.config.ts import type { Config } from 'jest';
const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['/tests//.ts', '**/?(.)+(spec|test).ts'], collectCoverageFrom: ['src//*.ts', '!src//*.d.ts'], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 }, }, setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'], };
export default config;
Vitest Configuration
// vitest.config.ts import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { globals: true, environment: 'node', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], }, setupFiles: ['./src/test/setup.ts'], }, });
Unit Testing Patterns
Testing Pure Functions
import { describe, it, expect } from 'vitest';
describe('Calculator', () => { describe('add', () => { it('should add two positive numbers', () => { expect(add(2, 3)).toBe(5); });
it('should handle zero', () => {
expect(add(0, 5)).toBe(5);
});
});
describe('divide', () => { it('should throw error when dividing by zero', () => { expect(() => divide(10, 0)).toThrow('Division by zero'); }); }); });
Testing Classes
describe('UserService', () => { let service: UserService;
beforeEach(() => { service = new UserService(); });
describe('create', () => { it('should create a new user', () => { const user = { id: '1', name: 'John', email: 'john@example.com' }; 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: 'john@example.com' };
service.create(user);
expect(() => service.create(user)).toThrow('User already exists');
});
}); });
Testing Async Functions
describe('ApiService', () => { beforeEach(() => { vi.clearAllMocks(); });
it('should fetch user successfully', async () => { const mockUser = { id: '1', name: 'John' }; global.fetch = vi.fn().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 () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: false }); await expect(service.fetchUser('999')).rejects.toThrow('User not found'); }); });
Mocking Patterns
Mocking Modules
vi.mock('nodemailer', () => ({ default: { createTransport: vi.fn(() => ({ sendMail: vi.fn().mockResolvedValue({ messageId: '123' }), })), }, }));
Dependency Injection
describe('UserService', () => { let service: UserService; let mockRepository: IUserRepository;
beforeEach(() => { mockRepository = { findById: vi.fn(), create: vi.fn(), }; service = new UserService(mockRepository); });
it('should return user if found', async () => { const mockUser = { id: '1', name: 'John' }; vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);
const user = await service.getUser('1');
expect(user).toEqual(mockUser);
expect(mockRepository.findById).toHaveBeenCalledWith('1');
}); });
Spying on Functions
let loggerSpy: any;
beforeEach(() => { loggerSpy = vi.spyOn(logger, 'info'); });
afterEach(() => { loggerSpy.mockRestore(); });
it('should log order processing', async () => { await service.processOrder('123'); expect(loggerSpy).toHaveBeenCalledWith('Processing order 123'); });
React Component Testing
import { render, screen, fireEvent } from '@testing-library/react';
describe('UserForm', () => { it('should render form inputs', () => { render(<UserForm onSubmit={vi.fn()} />);
expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
it('should call onSubmit with form data', () => { const onSubmit = vi.fn(); render(<UserForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByTestId('name-input'), {
target: { value: 'John Doe' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
}); });
Testing Hooks
import { renderHook, act } from '@testing-library/react';
describe('useCounter', () => { it('should increment count', () => { const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
}); });
Test Fixtures
import { faker } from '@faker-js/faker';
export function createUserFixture(overrides?: Partial<User>): User { return { id: faker.string.uuid(), name: faker.person.fullName(), email: faker.internet.email(), ...overrides, }; }
Best Practices
-
AAA Pattern - Arrange, Act, Assert
-
One assertion per test - Or logically related assertions
-
Descriptive test names - Describe what is being tested
-
Use beforeEach/afterEach - For setup and teardown
-
Mock external dependencies - Keep tests isolated
-
Test edge cases - Not just happy paths
-
Avoid implementation details - Test behavior
-
Keep tests fast - Mock slow operations
-
Maintain 80%+ coverage - Aim high
-
Clean up after tests - Prevent pollution
Common Patterns
// Testing promises await expect(service.fetchUser('invalid')).rejects.toThrow('Not found');
// Testing timers vi.useFakeTimers(); vi.advanceTimersByTime(1000); vi.useRealTimers();
// Snapshot testing expect(container.firstChild).toMatchSnapshot();
Detailed References
For comprehensive patterns, see:
-
references/integration-testing.md
-
references/react-testing-patterns.md
-
references/mocking-strategies.md
Resources
-
Jest: https://jestjs.io/
-
Vitest: https://vitest.dev/
-
Testing Library: https://testing-library.com/