TypeScript 单元测试入门与实践(二十三)
- 后端开发
- 6天前
- 7热度
- 0评论
在现代软件开发中,单元测试是确保代码质量和可靠性的重要手段。特别是对于 TypeScript 项目,其强大的类型系统与测试框架的结合,使得编写类型安全的测试代码变得更加容易。本文将详细介绍如何在 TypeScript 项目中进行单元测试,包括为什么需要单元测试、测试框架的配置、具体测试代码的编写以及一些最佳实践。
为什么需要单元测试
单元测试是验证代码正确性和稳定性的有效方法。通过编写单元测试,我们可以确保每个函数或类的行为符合预期,从而减少 bug 的出现。在 TypeScript 项目中,单元测试还具有以下优势:
- 类型安全:TypeScript 的类型系统可以在编译时捕获类型错误,IDE 还能提供智能提示,使测试代码更加可靠。
- 文档作用:测试代码本身就是最好的文档,通过阅读测试用例,开发者可以快速了解函数或类的预期行为。
- 快速反馈:单元测试可以快速发现回归问题,确保代码改动不会破坏现有功能。
测试框架配置
Jest 是目前最流行的 JavaScript 和 TypeScript 测试框架之一,它提供了丰富的功能和良好的社区支持。接下来,我们将详细介绍如何在 TypeScript 项目中配置和使用 Jest。
安装 Jest
首先,我们需要安装 Jest 及其相关的 TypeScript 预处理器 ts-jest。ts-jest 允许 Jest 直接运行 TypeScript 文件,无需手动编译。
npm install --save-dev jest ts-jest @types/jest配置 jest.config.js
接下来,我们需要配置 Jest 的测试环境。创建一个 jest.config.js 文件,并添加以下内容:
module.exports = {
// 使用 ts-jest 预设
preset: 'ts-jest',
// 测试环境:node 或 browser
testEnvironment: 'node',
// 测试文件目录
roots: ['
<rootDir>/src'],
// 测试文件匹配模式
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
// 支持的文件扩展名
moduleFileExtensions: ['ts', 'js', 'json'],
// 收集覆盖率的文件
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts' // 排除类型声明文件
]
};配置说明
- preset: 使用 ts-jest 预设,以便 Jest 能够处理 TypeScript 文件。
- testEnvironment: 指定测试环境,通常选择 node。
- roots: 指定测试文件的根目录。
- testMatch: 指定测试文件的匹配模式,通常放在 tests 目录或以 .test.ts 结尾。
- moduleFileExtensions: 指定支持的文件扩展名。
- collectCoverageFrom: 指定需要收集代码覆盖率的文件,排除类型声明文件。
测试函数
让我们从一个简单的计算器类开始,编写相应的测试代码。
编写业务代码
首先,创建一个 src/utils/calculator.ts 文件,定义一个计算器类:
// 计算器类
export class Calculator {
// 加法
add(a: number, b: number): number {
return a + b;
}
// 减法
subtract(a: number, b: number): number {
return a - b;
}
// 乘法
multiply(a: number, b: number): number {
return a * b;
}
// 除法
divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
}编写测试代码
接下来,创建一个 src/utils/calculator.test.ts 文件,编写测试用例:
import { Calculator } from './calculator';
// 测试套件:Calculator 类的测试
describe('Calculator', () => {
let calculator: Calculator;
// 每个测试前创建新的 Calculator 实例
beforeEach(() => {
calculator = new Calculator();
});
// 加法测试
describe('add', () => {
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(calculator.add(-1, 1)).toBe(0);
});
});
// 除法测试
describe('divide', () => {
it('should divide two numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});
it('should throw error when dividing by zero', () => {
// 期望抛出错误
expect(() => calculator.divide(10, 0)).toThrow();
});
});
});运行测试
运行以下命令来执行测试:
npx jest如果一切正常,你应该看到类似以下的输出:
Calculator
add
✓ should add two numbers
✓ should handle negative numbers
divide
✓ should divide two numbers
✓ should throw error when dividing by zero测试 Service
接下来,我们来看一个更复杂的例子,测试 Service 层的业务逻辑。
编写业务代码
创建一个 src/services/userService.ts 文件,定义一个用户服务类:
// 用户类型
export interface User {
id: number;
name: string;
}
// 用户服务类
export class UserService {
private users: User[] = [];
private nextId = 1;
// 创建用户
createUser(name: string): User {
const user = { id: this.nextId++, name };
this.users.push(user);
return user;
}
// 获取用户
getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
// 获取所有用户
getAllUsers(): User[] {
return [...this.users];
}
}编写测试代码
创建一个 src/services/userService.test.ts 文件,编写测试用例:
import { UserService } from './userService';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
// 创建用户测试
describe('createUser', () => {
it('should create a user with id', () => {
const user = service.createUser('Alice');
expect(user.id).toBe(1);
expect(user.name).toBe('Alice');
});
it('should increment id for each user', () => {
const user1 = service.createUser('Alice');
const user2 = service.createUser('Bob');
expect(user2.id).toBe(user1.id + 1);
});
});
// 获取用户测试
describe('getUser', () => {
it('should return user by id', () => {
const created = service.createUser('Alice');
const found = service.getUser(created.id);
// 使用可选链和 toBe
expect(found?.name).toBe('Alice');
});
it('should return undefined for non-existent id', () => {
const found = service.getUser(999);
expect(found).toBeUndefined();
});
});
});运行测试
再次运行测试命令:
npx jest你应该看到类似以下的输出:
UserService
createUser
✓ should create a user with id
✓ should increment id for each user
getUser
✓ should return user by id
✓ should return undefined for non-existent idMock
在单元测试中,我们经常需要模拟外部依赖,如外部 API、数据库等。Jest 提供了强大的 Mock 功能,可以帮助我们实现这一点。
使用 Mock 模拟函数
// Mock 函数:创建模拟函数
const mockCallback = jest.fn(x => x * 2);
// 使用模拟函数
[1, 2, 3].forEach(mockCallback);
// 验证函数被调用了 3 次
expect(mockCallback).toHaveBeenCalledTimes(3);
// 验证函数被调用时的参数
expect(mockCallback).toHaveBeenCalledWith(2);使用 Mock 模拟模块
// Mock 模块:模拟整个模块
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'Alice' }))
}));
// 使用模拟模块
import { fetchUser } from './api';
it('should fetch user', async () => {
const user = await fetchUser();
expect(user.id).toBe(1);
expect(user.name).toBe('Alice');
});运行结果
✓ should fetch user注意事项
- 测试文件位置:测试文件通常放在 tests 目录或使用 .test.ts 后缀。
- 测试命名:使用描述性的测试名称,说明预期行为。
- 独立测试:每个测试应该独立运行,不依赖其他测试。
- 覆盖率:关注核心业务逻辑的测试覆盖率。
最佳实践
快速、可靠:测试应该快速、可靠地运行。
相互独立:每个测试用例应该独立,使用 beforeEach 确保每个测试都有干净的状态。
AAA 原则:遵循 Arrange(准备)、Act(执行)、Assert(断言)的原则。
总结
单元测试是确保 TypeScript 代码质量的重要手段。通过使用 Jest 测试框架,我们可以轻松地编写类型安全的测试代码。本文介绍了如何配置 Jest、编写测试代码以及一些最佳实践。希望这些内容能帮助你在 TypeScript 项目中更好地进行单元测试,确保代码的稳定性和可靠性。