TypeScript 中的 Jest 测试
设置
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 },
},
};
基本测试
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)
// 自动模拟模块
jest.mock('./emailService');
import { sendEmail } from './emailService';
const mockSendEmail = sendEmail as jest.MockedFunction<typeof sendEmail>;
// 手动模拟
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);
// 在每个测试前重置模拟
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();
});
});
异步测试
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');
});
// 使用假定时器测试
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); // 前进超过重试延迟
const user = await promise;
expect(mockFetch).toHaveBeenCalledTimes(2);
jest.useRealTimers();
});
});
自定义匹配器
expect.extend({
toBeValidEmail(received: string) {
const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
return {
pass,
message: () => `Expected ${received} to be a valid email`,
};
},
});
// 使用
expect('alice@example.com').toBeValidEmail();
好的测试记录了预期行为,并支持无畏的重构。