使用Sinon进行Stub时测试用例无法正常运行
Sinon Stub不生效问题排查与解决
问题现象
测试bar函数时,使用Sinon为createFile创建Stub,但运行npm test时出现如下错误:
1) index.js stub function should stub log: AssertionError: expected greet to have been called exactly once, but it was called 0 times at Context.<anonymous> (test/lib/index.test.js:26:31) at process.processImmediate (node:internal/timers:478:21)
相关代码
index.js
async function bar(fileName) { await createFile(fileName); } function createFile(fileName) { // public return new Promise((resolve, reject) => { setTimeout(() => { resolve("done"); }, 100); }); } module.exports = { bar, createFile, };
index.test.js
const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); const expect = chai.expect; chai.use(sinonChai); const demo = require("./index.js"); describe("index.js", () => { context("stub function", () => { it("should stub createFile and callDB", async () => { // Stub createFile function in the scope where it's being imported (i.e., demo module) let stub = sinon.stub(demo, "createFile").resolves("create_stub"); await demo.bar('test.txt'); expect(stub).to.be.calledOnce; stub.restore(); }); }); });
package.json 依赖
"dependencies": { "chai": "^4.4.0", "cross-env": "^7.0.3", "rewire": "^7.0.0", "sinon": "^17.0.1", "sinon-chai": "^3.7.0" }
核心原因
bar函数内部直接调用的是模块内的本地createFile函数引用,而Sinon Stub的是模块导出的createFile函数。两者不是同一个引用,所以实际执行bar时不会触发Stub。
解决方案
方案一:修改模块内部调用为导出的函数
调整bar函数,让它调用模块导出的createFile:
async function bar(fileName) { // 改为调用module.exports上的createFile await module.exports.createFile(fileName); } function createFile(fileName) { return new Promise((resolve, reject) => { setTimeout(() => { resolve("done"); }, 100); }); } module.exports = { bar, createFile, };
方案二:使用rewire修改模块内部私有变量
利用rewire模块直接替换模块内部的createFile函数:
const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); const rewire = require('rewire'); const expect = chai.expect; chai.use(sinonChai); // 用rewire加载目标模块 const demo = rewire("./index.js"); describe("index.js", () => { context("stub function", () => { it("should stub createFile and callDB", async () => { // 创建Stub并替换模块内部的createFile const createFileStub = sinon.stub().resolves("create_stub"); demo.__set__('createFile', createFileStub); await demo.bar('test.txt'); expect(createFileStub).to.be.calledOnce; // 恢复原函数 demo.__restore__(); }); }); });
方案三:重构代码,抽离独立模块
将createFile抽离到单独的工具模块,这样测试时可以直接Stub该模块的导出:
- 新建
utils.js:
function createFile(fileName) { return new Promise((resolve, reject) => { setTimeout(() => { resolve("done"); }, 100); }); } module.exports = { createFile };
- 修改
index.js:
const { createFile } = require('./utils'); async function bar(fileName) { await createFile(fileName); } module.exports = { bar };
- 修改测试文件:
const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); const expect = chai.expect; const utils = require('./utils'); const demo = require("./index.js"); chai.use(sinonChai); describe("index.js", () => { context("stub function", () => { it("should stub createFile and callDB", async () => { const createFileStub = sinon.stub(utils, "createFile").resolves("create_stub"); await demo.bar('test.txt'); expect(createFileStub).to.be.calledOnce; createFileStub.restore(); }); }); });
内容的提问来源于stack exchange,提问作者rafl952
相关产品推荐
相关产品推荐

