如何用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
相关产品推荐
相关产品推荐

