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

如何用Sinon.js测试带参数的类方法?

问题描述

我用Sequelize ORM搭配Postgres开发Node.js + Express应用,测试工具采用Mocha、Chai和Sinon。目前需要测试userService实例的findOneUser(id)类方法,仅不传参数时测试抛出错误的逻辑能正常运行,但整体测试不完整。

待测试类方法(userService.js)

module.exports = class UserService {

  async findOneUser(id) {
    try {
      const user = await User.findOne({ where: { id: id } }); // 返回null时抛出错误

      if (!user) {
        throw createError(404, "User not found");
      }
      return user;
    } catch (err) {
      throw err
    }
  }
}

当前测试代码(userService.spec.js)

describe.only("findOne() throws error", () => {
  let userResult;
  const error = customError(404, "User not found"); // 自定义错误函数,包含状态码和信息
      before("before hook last it block issue withArgs" , async () => {

        // 已预先mock所有模型
        mockModels.User.findOne.withArgs({ where: { id: fakeId } }).resolves(null); // 期望根据fakeId参数调用该方法

        userResult = sinon.stub(userService, "findOneUser").throws(error); // 🤔不知如何为该类实例方法的stub传入参数🤔

      });

      after(() => {
        sinon.reset()
      });

      it("userService.findOneUser抛出错误(未传参数,可运行)🤔", () => {
        expect(userResult).to.throw(error);
      });
/// 下方测试仍无法运行
      it("调用User.findOne()传入错误参数,仍有问题🤯", () => {
        expect(mockModels.User.findOne).to.have.been.calledWith({ where: { id: fakeId } });
      })
});

核心问题

如何给被Stub的类方法findOneUser传入参数?或者有没有办法模拟调用该方法,让两个测试用例都能正常运行?

补充说明

已在describe块之前完成mockModels.User的Stub:

const UserModel = {
  findByPk: sinon.stub(),
  findOne: sinon.stub(),
  findAll: sinon.stub(),
  create: sinon.stub(),
  destroy: sinon.stub()
}

const mockModels = makeMockModels( { UserModel } );
// 将UserModel重命名为User以简化输入
delete Object.assign(mockModels, {['User']: mockModels['UserModel'] })['UserModel']

const UserService = proxyquire(servicePath, {
  "../models": mockModels
});
const userService = new UserService();
const fakeUser = {  update: sinon.stub() }

解决方案

你的核心问题是错误Stub了要测试的方法本身——应该直接调用真实的findOneUser方法,Stub的是它依赖的User.findOne,这样才能验证业务逻辑和依赖调用的正确性。

1. 重构测试结构,移除对findOneUser的Stub

删掉before里的sinon.stub(userService, "findOneUser").throws(error),直接调用真实方法。因为我们要测试的就是findOneUser的逻辑,Stub它等于跳过了真实逻辑的验证。

2. 调整测试用例

测试用例1:当User.findOne返回null时,findOneUser抛出404错误

由于方法是异步的,要用async/await配合Chai的rejectedWith断言:

it("当用户不存在时,findOneUser抛出404错误", async () => {
  await expect(userService.findOneUser(fakeId)).to.be.rejectedWith(error);
});

测试用例2:验证findOneUser正确调用了User.findOne

调用真实方法后,再断言依赖的Stub是否被正确调用:

it("findOneUser会以正确参数调用User.findOne", async () => {
  try {
    await userService.findOneUser(fakeId);
  } catch (e) {
    // 忽略错误,因为本来就期望它抛出
  }
  expect(mockModels.User.findOne).to.have.been.calledWith({ where: { id: fakeId } });
});

3. 完整测试代码示例

describe.only("findOneUser 错误场景测试", () => {
  const error = customError(404, "User not found");
  const fakeId = "non-existent-id"; // 定义一个不存在的用户ID

  before(() => {
    // 配置User.findOne的Stub,传入指定参数时返回null
    mockModels.User.findOne.withArgs({ where: { id: fakeId } }).resolves(null);
  });

  after(() => {
    sinon.reset(); // 重置所有Stub
  });

  it("当用户不存在时,抛出404错误", async () => {
    await expect(userService.findOneUser(fakeId)).to.be.rejectedWith(error);
  });

  it("调用User.findOne时传入了正确的id参数", async () => {
    await userService.findOneUser(fakeId).catch(() => {}); // 捕获错误不影响后续断言
    expect(mockModels.User.findOne).to.have.been.calledOnceWith({ where: { id: fakeId } });
  });
});

关键注意点

  • 不要Stub被测方法:被测方法是findOneUser,要验证它的真实逻辑,所以直接调用即可,Stub的是它依赖的User.findOne。
  • 异步方法测试:findOneUser是async方法,必须用async/await处理,Chai断言也要用异步版本(如rejectedWith)。
  • 错误处理:验证依赖调用的测试用例中,可用catch忽略错误,避免测试失败。

内容的提问来源于stack exchange,提问作者Mohamed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 15:26:07