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