JavaScript 测试替身
在软件测试中,我们经常需要对代码进行隔离测试。然而,有时我们的代码依赖于一些难以在测试环境中使用的组件,比如数据库、网络请求或者第三方服务。这时,测试替身(Test Doubles)就派上用场了 。
什么是测试替身?
测试替身是在测试过程中,用来替代真实依赖的对象。它们可以帮助我们:
- 加速测试执行(无需等待真实API响应)
- 避免不确定性(如网络问题)
- 模拟难以触发的场景(如服务器错误)
- 验证代码与依赖之间的交互
提示
测试替身的概念类似于电影中的特技替身,代替主角完成特定动作,但不需要具备主角的所有能力。
常见的测试替身类型
JavaScript测试中常用的测试替身主要有四种:
让我们逐一了解这些测试替身:
1. 假的对象(Fake)
假的对象是对真实对象的轻量级实现,有自己的业务逻辑,但通常采用更简单的方式实现。
示例:内存数据库替代真实数据库
// 真实的用户数据库类
class UserDatabase {
async findUserById(id) {
// 实际实现会连接数据库
return await db.query(`SELECT * FROM users WHERE id=${id}`);
}
}
// 假的用户数据库类(用于测试)
class FakeUserDatabase {
constructor() {
this.users = [
{ id: 1, name: "Alice", email: "[email protected]" },
{ id: 2, name: "Bob", email: "[email protected]" }
];
}
async findUserById(id) {
// 使用内存数据模拟数据库查询
const user = this.users.find(user => user.id === id);
return Promise.resolve(user);
}
}
// 在测试中使用
const userDb = new FakeUserDatabase();
const result = await userDb.findUserById(1);
console.log(result); // { id: 1, name: "Alice", email: "[email protected]" }
2. 桩(Stub)
桩是预编程的替身,返回预定义的响应,不包含业务逻辑。桩主要用于控制被测代码的间接输入。
示例:模拟API调用返回固定数据
// 需要测试的函数
async function getUserInfo(api, userId) {
const user = await api.fetchUser(userId);
return `${user.name} (${user.email})`;
}
// 测试代码
describe('getUserInfo', () => {
it('should format user info correctly', async () => {
// 创建API桩
const apiStub = {
fetchUser: async () => ({ name: "Alice", email: "[email protected]" })
};
const result = await getUserInfo(apiStub, 123);
expect(result).toBe("Alice ([email protected])");
});
});
3. 间谍(Spy)
间谍不仅可以像桩一样返回预定义的值,还可以记录函数被调用的方式、次数和传递的参数。间谍可以给真实对象"装上监听器"。
示例:监听函数调用
// 使用Jest进行测试
describe('购物车测试', () => {
it('添加商品时应调用保存方法', () => {
// 创建间谍
const localStorage = {
saveCart: jest.fn()
};
// 购物车类
class ShoppingCart {
constructor(storage) {
this.storage = storage;
this.items = [];
}
addItem(item) {
this.items.push(item);
this.storage.saveCart(this.items);
}
}
const cart = new ShoppingCart(localStorage);
cart.addItem({ id: 1, name: "书本", price: 29.99 });
// 验证saveCart被调用
expect(localStorage.saveCart).toHaveBeenCalledTimes(1);
// 验证调用参数
expect(localStorage.saveCart).toHaveBeenCalledWith([
{ id: 1, name: "书本", price: 29.99 }
]);
});
});
4. 模拟对象(Mock)
模拟对象是预编程的对象,具有预期行为和校验功能。它们不仅可以返回预设的值,还可以预设期望和验证。相比间谍,模拟对象更侧重于行为验证。
示例:模拟支付处理
// 使用Jest进行测试
describe('订单处理', () => {
it('