Sinon 教程
1. 核心知识点讲解
1.1 Sinon 简介
Sinon 是一个功能强大的 JavaScript 测试库,专注于提供独立的测试替身(test doubles),包括 Spy、Stub 和 Mock。它可以与任何 JavaScript 测试框架集成,帮助你编写更可靠的测试。
1.2 安装和配置
安装 Sinon:
npm install --save-dev sinon基本使用:
// 引入 Sinon
const sinon = require('sinon');
// 创建 Spy
const spy = sinon.spy();
// 创建 Stub
const stub = sinon.stub();
// 创建 Mock
const mock = sinon.mock(object);1.3 Spy(间谍)
Spy 用于观察函数的调用情况,包括调用次数、参数和返回值,但不会改变函数的行为。
基本用法:
const sinon = require('sinon');
// 监视现有函数
const obj = {
method: function() { return 'original'; }
};
const spy = sinon.spy(obj, 'method');
// 调用函数
obj.method();
obj.method('arg1', 'arg2');
// 检查调用情况
console.log(spy.called); // true
console.log(spy.callCount); // 2
console.log(spy.firstCall.args); // []
console.log(spy.secondCall.args); // ['arg1', 'arg2']
// 恢复原始函数
spy.restore();Spy 断言:
// 检查是否被调用
spy.called.should.be.true;
// 检查调用次数
spy.callCount.should.equal(2);
// 检查是否以特定参数调用
spy.calledWith('arg1', 'arg2').should.be.true;
// 检查是否以特定参数作为第一个参数调用
spy.calledWithMatch('arg1').should.be.true;
// 检查返回值
spy.returnValues.should.deep.equal(['original', 'original']);1.4 Stub(存根)
Stub 是 Spy 的扩展,不仅可以观察函数的调用情况,还可以替换函数的实现,控制其返回值或抛出的错误。
基本用法:
const sinon = require('sinon');
// 替换现有函数
const obj = {
method: function() { return 'original'; }
};
const stub = sinon.stub(obj, 'method');
// 设置返回值
stub.returns('stubbed');
// 调用函数
console.log(obj.method()); // 'stubbed'
// 设置条件返回值
stub.withArgs('arg1').returns('value1');
stub.withArgs('arg2').returns('value2');
console.log(obj.method('arg1')); // 'value1'
console.log(obj.method('arg2')); // 'value2'
// 设置抛出错误
stub.withArgs('error').throws(new Error('Stub error'));
// try {
// obj.method('error');
// } catch (e) {
// console.log(e.message); // 'Stub error'
// }
// 恢复原始函数
stub.restore();Stub 用于异步函数:
// 设置 Promise 返回值
stub.resolves('async result');
// 或设置 Promise 拒绝
stub.rejects(new Error('Async error'));
// 使用 async/await
async function test() {
const result = await obj.method();
console.log(result); // 'async result'
}1.5 Mock(模拟)
Mock 是 Stub 的进一步扩展,它不仅可以替换函数的实现,还可以预先定义期望的调用行为,并在测试结束时验证这些期望是否被满足。
基本用法:
const sinon = require('sinon');
// 创建 Mock 对象
const obj = {
method1: function() {},
method2: function() {}
};
const mock = sinon.mock(obj);
// 设置期望
mock.expects('method1').once().withArgs('arg1').returns('value1');
mock.expects('method2').twice();
// 调用函数
obj.method1('arg1');
obj.method2();
obj.method2();
// 验证期望
mock.verify();
// 恢复原始函数
mock.restore();Mock 验证:
// 验证所有期望都被满足
mock.verify();
// 如果期望未被满足,会抛出错误
// 例如:Expected obj.method1 to be called once but was called 0 times1.6 计时器控制
Sinon 可以控制 JavaScript 的计时器(setTimeout、setInterval 等),使时间相关的测试更加可靠。
基本用法:
const sinon = require('sinon');
// 替换全局计时器
const clock = sinon.useFakeTimers();
// 测试代码
function delayedFunction() {
let called = false;
setTimeout(() => {
called = true;
}, 1000);
return () => called;
}
const checkCalled = delayedFunction();
console.log(checkCalled()); // false
// 快进时间
clock.tick(1000);
console.log(checkCalled()); // true
// 恢复原始计时器
clock.restore();setInterval 测试:
const sinon = require('sinon');
const clock = sinon.useFakeTimers();
let count = 0;
const intervalId = setInterval(() => {
count++;
}, 100);
console.log(count); // 0
clock.tick(100);
console.log(count); // 1
clock.tick(100);
console.log(count); // 2
clearInterval(intervalId);
clock.restore();1.7 沙箱(Sandbox)
沙箱可以帮助你管理多个测试替身,避免手动恢复每个替身。
基本用法:
const sinon = require('sinon');
// 创建沙箱
const sandbox = sinon.createSandbox();
// 在沙箱中创建 Spy、Stub 或 Mock
const obj = {
method1: function() {},
method2: function() {}
};
const spy = sandbox.spy(obj, 'method1');
const stub = sandbox.stub(obj, 'method2').returns('stubbed');
// 调用函数
obj.method1();
obj.method2();
// 清理所有测试替身
sandbox.restore();
// 现在所有函数都已恢复原始状态在测试框架中使用沙箱:
describe('测试套件', function() {
let sandbox;
beforeEach(function() {
sandbox = sinon.createSandbox();
});
afterEach(function() {
sandbox.restore();
});
it('测试用例', function() {
// 在沙箱中创建测试替身
const spy = sandbox.spy(obj, 'method');
// 测试代码
});
});2. 实用案例分析
2.1 Spy 使用案例
场景: 测试一个函数是否正确调用了其他函数
实现:
// src/userService.js
function notifyUser(user, message) {
console.log(`通知用户 ${user.name}: ${message}`);
}
function createUser(userData) {
const user = {
id: Math.floor(Math.random() * 10000),
...userData
};
notifyUser(user, '用户创建成功');
return user;
}
module.exports = {
createUser,
notifyUser
};
// test/userService.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { createUser, notifyUser } = require('../src/userService');
describe('用户服务测试', function() {
it('创建用户应该调用 notifyUser 函数', function() {
// 监视 notifyUser 函数
const spy = sinon.spy(notifyUser);
// 保存原始函数
const originalNotifyUser = notifyUser;
// 替换模块中的函数
require.cache[require.resolve('../src/userService')].exports.notifyUser = spy;
try {
// 创建用户
const user = createUser({ name: '张三', email: 'zhangsan@example.com' });
// 验证 notifyUser 被调用
expect(spy.called).to.be.true;
expect(spy.calledOnce).to.be.true;
// 验证调用参数
expect(spy.firstCall.args[0]).to.deep.include({ name: '张三' });
expect(spy.firstCall.args[1]).to.equal('用户创建成功');
} finally {
// 恢复原始函数
require.cache[require.resolve('../src/userService')].exports.notifyUser = originalNotifyUser;
}
});
});2.2 Stub 使用案例
场景: 测试一个依赖外部 API 的函数
实现:
// src/apiService.js
const axios = require('axios');
async function fetchUser(userId) {
try {
const response = await axios.get(`https://api.example.com/users/${userId}`);
return response.data;
} catch (error) {
throw new Error(`获取用户失败: ${error.message}`);
}
}
module.exports = { fetchUser };
// test/apiService.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const axios = require('axios');
const { fetchUser } = require('../src/apiService');
describe('API 服务测试', function() {
let axiosGetStub;
beforeEach(function() {
// 替换 axios.get 方法
axiosGetStub = sinon.stub(axios, 'get');
});
afterEach(function() {
// 恢复原始方法
axiosGetStub.restore();
});
it('成功获取用户信息', async function() {
// 设置成功响应
const mockUser = { id: 1, name: '张三', email: 'zhangsan@example.com' };
axiosGetStub.resolves({ data: mockUser });
// 调用函数
const user = await fetchUser(1);
// 验证结果
expect(user).to.deep.equal(mockUser);
expect(axiosGetStub.calledOnce).to.be.true;
expect(axiosGetStub.firstCall.args[0]).to.equal('https://api.example.com/users/1');
});
it('获取用户失败时应该抛出错误', async function() {
// 设置失败响应
axiosGetStub.rejects(new Error('网络错误'));
// 验证错误
try {
await fetchUser(1);
expect.fail('应该抛出错误');
} catch (error) {
expect(error.message).to.equal('获取用户失败: 网络错误');
expect(axiosGetStub.calledOnce).to.be.true;
}
});
});2.3 Mock 使用案例
场景: 测试一个复杂对象的方法调用顺序和参数
实现:
// src/orderService.js
class OrderService {
constructor(inventoryService, paymentService, notificationService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
async processOrder(order) {
// 1. 检查库存
const isAvailable = await this.inventoryService.checkAvailability(order.items);
if (!isAvailable) {
throw new Error('库存不足');
}
// 2. 处理支付
const paymentResult = await this.paymentService.charge(order.total, order.paymentInfo);
if (!paymentResult.success) {
throw new Error('支付失败');
}
// 3. 更新库存
await this.inventoryService.reserveItems(order.items);
// 4. 发送通知
await this.notificationService.sendConfirmation(order.customer);
return { success: true, orderId: order.id };
}
}
module.exports = OrderService;
// test/orderService.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const OrderService = require('../src/orderService');
describe('订单服务测试', function() {
let inventoryServiceMock;
let paymentServiceMock;
let notificationServiceMock;
let orderService;
beforeEach(function() {
// 创建 Mock 对象
inventoryServiceMock = sinon.mock({
checkAvailability: async () => {},
reserveItems: async () => {}
});
paymentServiceMock = sinon.mock({
charge: async () => {}
});
notificationServiceMock = sinon.mock({
sendConfirmation: async () => {}
});
// 创建订单服务实例
orderService = new OrderService(
inventoryServiceMock.object,
paymentServiceMock.object,
notificationServiceMock.object
);
});
afterEach(function() {
// 恢复所有 Mock
inventoryServiceMock.restore();
paymentServiceMock.restore();
notificationServiceMock.restore();
});
it('处理订单应该按正确顺序调用服务', async function() {
const order = {
id: 1,
items: [{ id: 1, quantity: 2 }],
total: 100,
paymentInfo: { cardNumber: '1234567890123456' },
customer: { id: 1, name: '张三' }
};
// 设置期望
inventoryServiceMock.expects('checkAvailability')
.once()
.withArgs(order.items)
.resolves(true);
paymentServiceMock.expects('charge')
.once()
.withArgs(order.total, order.paymentInfo)
.resolves({ success: true });
inventoryServiceMock.expects('reserveItems')
.once()
.withArgs(order.items)
.resolves();
notificationServiceMock.expects('sendConfirmation')
.once()
.withArgs(order.customer)
.resolves();
// 处理订单
const result = await orderService.processOrder(order);
// 验证结果
expect(result).to.deep.equal({ success: true, orderId: order.id });
// 验证所有期望都被满足
inventoryServiceMock.verify();
paymentServiceMock.verify();
notificationServiceMock.verify();
});
it('库存不足时应该抛出错误', async function() {
const order = {
id: 1,
items: [{ id: 1, quantity: 2 }],
total: 100,
paymentInfo: { cardNumber: '1234567890123456' },
customer: { id: 1, name: '张三' }
};
// 设置期望
inventoryServiceMock.expects('checkAvailability')
.once()
.withArgs(order.items)
.resolves(false);
// 验证错误
try {
await orderService.processOrder(order);
expect.fail('应该抛出错误');
} catch (error) {
expect(error.message).to.equal('库存不足');
inventoryServiceMock.verify();
// 验证后续方法没有被调用
paymentServiceMock.verify(); // 应该没有期望
}
});
});3. 代码示例
3.1 Spy、Stub 和 Mock 对比
Spy 示例:
const sinon = require('sinon');
// 监视 console.log
const spy = sinon.spy(console, 'log');
console.log('Hello, World!');
console.log('Hello, Sinon!');
console.log('调用次数:', spy.callCount); // 2
console.log('第一次调用参数:', spy.firstCall.args); // ['Hello, World!']
console.log('第二次调用参数:', spy.secondCall.args); // ['Hello, Sinon!']
// 恢复原始函数
spy.restore();Stub 示例:
const sinon = require('sinon');
// 存根 Math.random
const stub = sinon.stub(Math, 'random').returns(0.5);
console.log(Math.random()); // 0.5
console.log(Math.random()); // 0.5
// 恢复原始函数
stub.restore();
console.log(Math.random()); // 随机值Mock 示例:
const sinon = require('sinon');
// 模拟对象
const userService = {
getUser: function(id) {},
updateUser: function(id, data) {}
};
const mock = sinon.mock(userService);
// 设置期望
mock.expects('getUser').once().withArgs(1).returns({ id: 1, name: '张三' });
mock.expects('updateUser').once().withArgs(1, { name: '李四' }).returns({ id: 1, name: '李四' });
// 调用函数
const user = userService.getUser(1);
console.log(user); // { id: 1, name: '张三' }
const updatedUser = userService.updateUser(1, { name: '李四' });
console.log(updatedUser); // { id: 1, name: '李四' }
// 验证期望
mock.verify();
// 恢复原始函数
mock.restore();3.2 计时器测试示例
const sinon = require('sinon');
const { expect } = require('chai');
describe('计时器测试', function() {
let clock;
beforeEach(function() {
// 替换全局计时器
clock = sinon.useFakeTimers();
});
afterEach(function() {
// 恢复原始计时器
clock.restore();
});
it('setTimeout 应该在指定时间后执行', function() {
let called = false;
setTimeout(() => {
called = true;
}, 1000);
expect(called).to.be.false;
// 快进 999 毫秒
clock.tick(999);
expect(called).to.be.false;
// 快进 1 毫秒,总共 1000 毫秒
clock.tick(1);
expect(called).to.be.true;
});
it('setInterval 应该重复执行', function() {
let count = 0;
setInterval(() => {
count++;
}, 100);
expect(count).to.equal(0);
// 快进 100 毫秒
clock.tick(100);
expect(count).to.equal(1);
// 快进 100 毫秒,总共 200 毫秒
clock.tick(100);
expect(count).to.equal(2);
// 快进 500 毫秒,总共 700 毫秒
clock.tick(500);
expect(count).to.equal(7);
});
it('clearTimeout 应该取消计时器', function() {
let called = false;
const timeoutId = setTimeout(() => {
called = true;
}, 1000);
// 取消计时器
clearTimeout(timeoutId);
// 快进 1000 毫秒
clock.tick(1000);
expect(called).to.be.false;
});
});3.3 沙箱使用示例
const sinon = require('sinon');
const { expect } = require('chai');
describe('沙箱测试', function() {
let sandbox;
beforeEach(function() {
// 创建沙箱
sandbox = sinon.createSandbox();
});
afterEach(function() {
// 清理沙箱中的所有测试替身
sandbox.restore();
});
it('使用沙箱管理多个测试替身', function() {
// 在沙箱中创建 Spy
const spy = sandbox.spy(console, 'log');
// 在沙箱中创建 Stub
const stub = sandbox.stub(Math, 'random').returns(0.5);
// 在沙箱中创建 Mock
const obj = { method: function() {} };
const mock = sandbox.mock(obj);
mock.expects('method').once();
// 测试代码
console.log('Hello');
console.log('Random:', Math.random());
obj.method();
// 验证
expect(spy.calledTwice).to.be.true;
expect(Math.random()).to.equal(0.5);
mock.verify();
});
it('沙箱应该在 afterEach 中清理所有测试替身', function() {
// 在沙箱中创建 Stub
sandbox.stub(Math, 'random').returns(0.5);
expect(Math.random()).to.equal(0.5);
});
it('上一个测试的测试替身应该被清理', function() {
// 验证 Math.random 已经恢复原始行为
const randomValue = Math.random();
expect(randomValue).to.be.a('number');
expect(randomValue).to.be.lessThan(1);
expect(randomValue).to.be.greaterThanOrEqual(0);
});
});3.4 与测试框架集成
与 Mocha 集成:
const { expect } = require('chai');
const sinon = require('sinon');
describe('与 Mocha 集成测试', function() {
let stub;
beforeEach(function() {
stub = sinon.stub(Math, 'random');
});
afterEach(function() {
stub.restore();
});
it('测试 Math.random 存根', function() {
stub.returns(0.42);
expect(Math.random()).to.equal(0.42);
});
it('测试 Math.random 另一个存根值', function() {
stub.returns(0.99);
expect(Math.random()).to.equal(0.99);
});
});与 Jest 集成:
const sinon = require('sinon');
describe('与 Jest 集成测试', () => {
let stub;
beforeEach(() => {
stub = sinon.stub(Math, 'random');
});
afterEach(() => {
stub.restore();
});
test('测试 Math.random 存根', () => {
stub.returns(0.42);
expect(Math.random()).toBe(0.42);
});
test('测试 Math.random 另一个存根值', () => {
stub.returns(0.99);
expect(Math.random()).toBe(0.99);
});
});4. 总结
Sinon 是一个功能强大的 JavaScript 测试库,提供了三种主要的测试替身:
- Spy:用于观察函数的调用情况,包括调用次数、参数和返回值
- Stub:用于替换函数的实现,控制其返回值或抛出的错误
- Mock:用于预先定义期望的调用行为,并在测试结束时验证这些期望
此外,Sinon 还提供了计时器控制功能,可以精确控制时间相关的测试,以及沙箱功能,可以方便地管理多个测试替身。
通过学习本教程,你可以开始在项目中使用 Sinon 编写更加可靠、可预测的测试,特别是在处理外部依赖、时间相关逻辑和复杂的函数调用链时。
5. 进一步学习资源
希望本教程对你学习 Sinon 有所帮助!