You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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该模块的导出:

  1. 新建utils.js:
function createFile(fileName) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("done");
    }, 100);
  });
}

module.exports = { createFile };
  1. 修改index.js:
const { createFile } = require('./utils');

async function bar(fileName) {
  await createFile(fileName);
}

module.exports = { bar };
  1. 修改测试文件:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 15:32:37