Jest Testing in TypeScript
Setup
npm install -D jest @types/jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**'],
coverageThresholds: {
global: { lines: 80, functions: 80, branches: 70 },
},
};
Basic Tests
import { calculateDiscount, formatPrice } from './pricing';
describe('calculateDiscount', () => {
it('should apply percentage discount', () => {
expect(calculateDiscount(100, 10)).toBe(90);
expect(calculateDiscount(50, 25)).toBe(37.5);
});
it('should return full price when discount is 0', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('should throw for invalid discount', () => {
expect(() => calculateDiscount(100, 110)).toThrow('Discount cannot exceed 100%');
expect(() => calculateDiscount(100, -5)).toThrow('Discount cannot be negative');
});
it.each([
[100, 10, 90],
[200, 50, 100],
[75, 20, 60],
])('calculateDiscount(%d, %d) = %d', (price, discount, expected) => {
expect(calculateDiscount(price, discount)).toBe(expected);
});
});
Mocking
// Auto mock module
jest.mock('./emailService');
import { sendEmail } from './emailService';
const mockSendEmail = sendEmail as jest.MockedFunction<typeof sendEmail>;
// Manual mock
jest.mock('./database', () => ({
query: jest.fn(),
insert: jest.fn(),
close: jest.fn(),
}));
describe('UserService', () => {
let userService: UserService;
let mockUserRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockUserRepo = {
findById: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
userService = new UserService(mockUserRepo, mockSendEmail);
// Reset mocks before each test
jest.clearAllMocks();
});
it('should send welcome email on registration', async () => {
const savedUser = { id: '1', email: 'alice@example.com', name: 'Alice' };
mockUserRepo.save.mockResolvedValue(savedUser);
mockSendEmail.mockResolvedValue(undefined);
await userService.register({ email: 'alice@example.com', name: 'Alice' });
expect(mockSendEmail).toHaveBeenCalledWith({
to: 'alice@example.com',
subject: 'Welcome!',
body: expect.stringContaining('Alice'),
});
});
it('should not send email if save fails', async () => {
mockUserRepo.save.mockRejectedValue(new Error('DB error'));
await expect(userService.register({ email: 'alice@example.com', name: 'Alice' }))
.rejects.toThrow('DB error');
expect(mockSendEmail).not.toHaveBeenCalled();
});
});
Async Testing
describe('fetchUser', () => {
it('returns user on success', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
});
const user = await fetchUser('1');
expect(user).toEqual({ id: '1', name: 'Alice' });
});
it('throws on network error', async () => {
global.fetch = jest.fn().mockRejectedValue(new Error('Network Error'));
await expect(fetchUser('1')).rejects.toThrow('Network Error');
});
// Testing with fake timers
it('retries after delay', async () => {
jest.useFakeTimers();
const mockFetch = jest.fn()
.mockRejectedValueOnce(new Error('timeout'))
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: '1' }) });
global.fetch = mockFetch;
const promise = fetchUserWithRetry('1');
jest.advanceTimersByTime(1000); // Advance past retry delay
const user = await promise;
expect(mockFetch).toHaveBeenCalledTimes(2);
jest.useRealTimers();
});
});
Custom Matchers
expect.extend({
toBeValidEmail(received: string) {
const pass = /^[^s@]+@[^s@]+.[^s@]+$/.test(received);
return {
pass,
message: () => `Expected ${received} to be a valid email`,
};
},
});
// Usage
expect('alice@example.com').toBeValidEmail();
Good tests document expected behavior and enable fearless refactoring.