Mocha Mock 和 Stub #
为什么需要 Mock? #
在测试中,我们经常需要隔离外部依赖:
text
┌─────────────────────────────────────────────────────────────┐
│ 测试隔离的原因 │
├─────────────────────────────────────────────────────────────┤
│ 1. 避免真实 API 调用(速度慢、不稳定、有费用) │
│ 2. 避免真实数据库操作(数据污染、清理困难) │
│ 3. 模拟各种场景(成功、失败、超时等) │
│ 4. 验证函数调用(参数、次数、顺序) │
└─────────────────────────────────────────────────────────────┘
Sinon.js 简介 #
Sinon.js 是最流行的 JavaScript 测试辅助库,提供三种主要功能:
| 功能 | 描述 | 使用场景 |
|---|---|---|
| Spy | 监视函数调用 | 验证函数是否被调用 |
| Stub | 替换函数行为 | 控制函数返回值 |
| Mock | 完整的模拟对象 | 验证预期行为 |
安装 Sinon #
bash
npm install --save-dev sinon
Spy(间谍) #
Spy 用于监视函数调用,不改变函数原有行为。
基本用法 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
describe('Spy', function() {
it('should track function calls', function() {
const callback = sinon.spy();
callback();
callback('hello');
callback('hello', 'world');
expect(callback.called).to.be.true;
expect(callback.callCount).to.equal(3);
expect(callback.calledWith('hello')).to.be.true;
expect(callback.calledWith('hello', 'world')).to.be.true;
});
it('should track call arguments', function() {
const spy = sinon.spy();
spy(1, 2, 3);
spy('a', 'b');
expect(spy.firstCall.args).to.deep.equal([1, 2, 3]);
expect(spy.secondCall.args).to.deep.equal(['a', 'b']);
expect(spy.getCall(0).args).to.deep.equal([1, 2, 3]);
});
});
监视对象方法 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
describe('Spy Object Methods', function() {
const user = {
getName: function() {
return this.name;
},
setName: function(name) {
this.name = name;
}
};
it('should spy on object method', function() {
const spy = sinon.spy(user, 'getName');
user.setName('John');
const result = user.getName();
expect(result).to.equal('John');
expect(spy.calledOnce).to.be.true;
expect(spy.returned('John')).to.be.true;
spy.restore(); // 恢复原始方法
});
});
Spy 断言 #
javascript
describe('Spy Assertions', function() {
it('should verify call count', function() {
const spy = sinon.spy();
spy();
spy();
spy();
sinon.assert.calledThrice(spy);
// 或使用 Chai
expect(spy.calledThrice).to.be.true;
});
it('should verify call arguments', function() {
const spy = sinon.spy();
spy('hello', 'world');
sinon.assert.calledWith(spy, 'hello', 'world');
sinon.assert.calledWithExactly(spy, 'hello', 'world');
});
it('should verify call order', function() {
const spy1 = sinon.spy();
const spy2 = sinon.spy();
spy1();
spy2();
sinon.assert.callOrder(spy1, spy2);
});
it('should verify return value', function() {
const spy = sinon.spy(function() { return 42; });
spy();
sinon.assert.returned(spy, 42);
});
});
Stub(存根) #
Stub 用于替换函数实现,完全控制函数行为。
基本用法 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
describe('Stub', function() {
it('should return fixed value', function() {
const stub = sinon.stub().returns(42);
expect(stub()).to.equal(42);
expect(stub()).to.equal(42);
});
it('should return different values', function() {
const stub = sinon.stub();
stub.onFirstCall().returns(1);
stub.onSecondCall().returns(2);
stub.returns(3);
expect(stub()).to.equal(1);
expect(stub()).to.equal(2);
expect(stub()).to.equal(3);
expect(stub()).to.equal(3);
});
it('should throw error', function() {
const stub = sinon.stub().throws(new Error('Something went wrong'));
expect(() => stub()).to.throw('Something went wrong');
});
it('should call callback', function() {
const stub = sinon.stub();
stub.callsArgWith(1, 'result');
const callback = sinon.spy();
stub('input', callback);
sinon.assert.calledWith(callback, 'result');
});
});
替换对象方法 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
describe('Stub Object Methods', function() {
const database = {
find: function(id) {
// 实际数据库查询
return db.query('SELECT * FROM users WHERE id = ?', [id]);
},
save: function(data) {
// 实际数据库保存
return db.insert('users', data);
}
};
afterEach(function() {
sinon.restore();
});
it('should stub database find', async function() {
const stub = sinon.stub(database, 'find').resolves({ id: 1, name: 'John' });
const result = await database.find(1);
expect(result).to.deep.equal({ id: 1, name: 'John' });
sinon.assert.calledOnceWithExactly(stub, 1);
});
it('should stub database save', async function() {
const stub = sinon.stub(database, 'save').resolves({ id: 1, affectedRows: 1 });
const result = await database.save({ name: 'John' });
expect(result.affectedRows).to.equal(1);
});
it('should stub to throw error', async function() {
sinon.stub(database, 'find').rejects(new Error('Connection failed'));
await expect(database.find(1))
.to.be.rejectedWith('Connection failed');
});
});
Stub 不同实现 #
javascript
describe('Stub Implementations', function() {
it('should use custom implementation', function() {
const stub = sinon.stub();
stub.callsFake(function(a, b) {
return a + b;
});
expect(stub(1, 2)).to.equal(3);
expect(stub(5, 3)).to.equal(8);
});
it('should return values based on arguments', function() {
const stub = sinon.stub();
stub.withArgs(1).returns('one');
stub.withArgs(2).returns('two');
stub.returns('other');
expect(stub(1)).to.equal('one');
expect(stub(2)).to.equal('two');
expect(stub(3)).to.equal('other');
});
it('should resolve with different values', async function() {
const stub = sinon.stub();
stub.withArgs('valid').resolves({ success: true });
stub.withArgs('invalid').rejects(new Error('Invalid input'));
await expect(stub('valid')).to.eventually.deep.equal({ success: true });
await expect(stub('invalid')).to.be.rejectedWith('Invalid input');
});
});
Mock(模拟) #
Mock 是预编程的预期行为,用于验证完整的交互。
基本用法 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
describe('Mock', function() {
it('should verify expectations', function() {
const myObject = {
method1: function() {},
method2: function() {}
};
const mock = sinon.mock(myObject);
// 设置预期
mock.expects('method1').once().returns(42);
mock.expects('method2').never();
// 执行代码
const result = myObject.method1();
// 验证预期
expect(result).to.equal(42);
mock.verify();
});
it('should verify call arguments', function() {
const obj = {
save: function(data) {}
};
const mock = sinon.mock(obj);
mock.expects('save').once().withArgs({ name: 'John' });
obj.save({ name: 'John' });
mock.verify();
});
});
Mock 预期 #
javascript
describe('Mock Expectations', function() {
it('should verify call count', function() {
const obj = { method: function() {} };
const mock = sinon.mock(obj);
mock.expects('method').twice();
obj.method();
obj.method();
mock.verify();
});
it('should verify call order', function() {
const obj = {
init: function() {},
process: function() {}
};
const mock = sinon.mock(obj);
mock.expects('init').once().calledBefore('process');
mock.expects('process').once();
obj.init();
obj.process();
mock.verify();
});
it('should verify return value', function() {
const obj = { getValue: function() {} };
const mock = sinon.mock(obj);
mock.expects('getValue').returns(100);
expect(obj.getValue()).to.equal(100);
mock.verify();
});
});
Sinon 实际应用 #
测试 API 调用 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
const axios = require('axios');
const UserService = require('../src/user-service');
describe('UserService', function() {
let axiosStub;
let userService;
beforeEach(function() {
axiosStub = sinon.stub(axios, 'get');
userService = new UserService();
});
afterEach(function() {
sinon.restore();
});
it('should fetch user by id', async function() {
axiosStub.resolves({ data: { id: 1, name: 'John' } });
const user = await userService.getUser(1);
expect(user).to.deep.equal({ id: 1, name: 'John' });
sinon.assert.calledOnceWithExactly(axiosStub, '/api/users/1');
});
it('should handle not found', async function() {
axiosStub.rejects({ response: { status: 404 } });
await expect(userService.getUser(999))
.to.be.rejectedWith('User not found');
});
it('should handle network error', async function() {
axiosStub.rejects(new Error('Network Error'));
await expect(userService.getUser(1))
.to.be.rejectedWith('Network Error');
});
});
测试数据库操作 #
javascript
const sinon = require('sinon');
const { expect } = require('chai');
const UserRepository = require('../src/user-repository');
describe('UserRepository', function() {
let repository;
let dbStub;
beforeEach(function() {
repository = new UserRepository();
dbStub = sinon.stub(repository, 'db');
});
afterEach(function() {
sinon.restore();
});
describe('findById', function() {
it('should return user when found', async function() {
dbStub.query.resolves([{ id: 1, name: 'John' }]);
const user = await repository.findById(1);
expect(user).to.deep.equal({ id: 1, name: 'John' });
});
it('should return null when not found', async function() {
dbStub.query.resolves([]);
const user = await repository.findById(999);
expect(user).to.be.null;
});
});
describe('save', function() {
it('should insert new user', async function() {
dbStub.insert.resolves({ insertId: 1 });
const result = await repository.save({ name: 'John' });
expect(result.id).to.equal(1);
});
});
});
测试回调函数 #
javascript
describe('Callback Testing', function() {
it('should call callback with result', function() {
const callback = sinon.spy();
const stub = sinon.stub().callsArgWith(1, 'success');
function processData(data, cb) {
// 模拟异步处理
setTimeout(() => cb('success'), 100);
}
processData('data', callback);
// 需要等待异步完成
setTimeout(() => {
sinon.assert.calledWith(callback, 'success');
}, 150);
});
it('should call callback with error', function() {
const callback = sinon.spy();
const error = new Error('Processing failed');
function processData(data, cb) {
cb(error);
}
processData('data', callback);
sinon.assert.calledWith(callback, error);
});
});
测试定时器 #
javascript
describe('Timer Testing', function() {
let clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
});
afterEach(function() {
clock.restore();
});
it('should call function after delay', function() {
const callback = sinon.spy();
setTimeout(callback, 1000);
clock.tick(999);
expect(callback.called).to.be.false;
clock.tick(1);
expect(callback.called).to.be.true;
});
it('should work with setInterval', function() {
const callback = sinon.spy();
const intervalId = setInterval(callback, 100);
clock.tick(350);
expect(callback.callCount).to.equal(3);
clearInterval(intervalId);
});
it('should work with async functions', async function() {
let resolved = false;
const promise = new Promise(resolve => {
setTimeout(() => {
resolved = true;
resolve();
}, 1000);
});
clock.tick(1000);
await promise;
expect(resolved).to.be.true;
});
});
Sinon Sandboxes #
使用 sandbox 可以自动清理所有 stub 和 spy:
javascript
const sinon = require('sinon');
describe('Using Sandbox', function() {
let sandbox;
beforeEach(function() {
sandbox = sinon.createSandbox();
});
afterEach(function() {
sandbox.restore();
});
it('should auto restore stubs', function() {
const obj = { method: function() {} };
const stub = sandbox.stub(obj, 'method').returns(42);
expect(obj.method()).to.equal(42);
// afterEach 会自动恢复
});
it('should auto restore spies', function() {
const callback = sandbox.spy();
callback();
expect(callback.called).to.be.true;
});
it('should auto restore mocks', function() {
const obj = { method: function() {} };
const mock = sandbox.mock(obj);
mock.expects('method').once();
obj.method();
mock.verify();
});
});
Sinon-Chai 插件 #
sinon-chai 提供 Chai 风格的断言:
bash
npm install --save-dev sinon-chai
javascript
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
chai.use(sinonChai);
const { expect } = chai;
describe('Sinon-Chai', function() {
it('should use chai-style assertions', function() {
const spy = sinon.spy();
spy('hello');
// sinon-chai 风格
expect(spy).to.have.been.called;
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith('hello');
});
it('should work with stubs', function() {
const stub = sinon.stub().returns(42);
stub();
expect(stub).to.have.been.called;
expect(stub).to.have.returned(42);
});
});
最佳实践 #
1. 始终恢复 Stub 和 Spy #
javascript
describe('Best Practices', function() {
let stub;
beforeEach(function() {
stub = sinon.stub(obj, 'method');
});
afterEach(function() {
stub.restore();
// 或使用 sandbox
// sandbox.restore();
});
it('test', function() {
// ...
});
});
2. 使用 Sandbox 管理生命周期 #
javascript
describe('Sandbox Pattern', function() {
let sandbox;
beforeEach(function() {
sandbox = sinon.createSandbox();
});
afterEach(function() {
sandbox.restore();
});
it('test', function() {
const stub = sandbox.stub(obj, 'method');
const spy = sandbox.spy();
// 自动清理
});
});
3. 选择合适的工具 #
javascript
// Spy:只需要监视调用
const spy = sinon.spy(obj, 'method');
// 不改变行为,只记录调用
// Stub:需要控制返回值
const stub = sinon.stub(obj, 'method').returns(42);
// 完全替换行为
// Mock:需要验证预期行为
const mock = sinon.mock(obj);
mock.expects('method').once().returns(42);
// 预设预期,最后验证
4. 避免过度 Mock #
javascript
// ❌ 过度 Mock
it('should work', function() {
const stub1 = sinon.stub(obj, 'method1');
const stub2 = sinon.stub(obj, 'method2');
const stub3 = sinon.stub(obj, 'method3');
// 测试变得脆弱
});
// ✅ 只 Mock 外部依赖
it('should work', function() {
sinon.stub(externalApi, 'fetch').resolves(data);
// 只 Mock 外部 API,内部逻辑真实测试
});
下一步 #
现在你已经掌握了 Mock 和 Stub 的使用方法,接下来学习 配置与优化 了解 Mocha 的高级配置!
最后更新:2026-03-28