如何使用Mocha与Sinon模拟类async/await方法并编写路由测试用例
我明白你现在的困扰:明明想用Sinon模拟Detector类的方法,结果还是触发了原方法,而且不知道怎么处理async/await的模拟。下面我会结合实际代码示例,一步步教你写好路由的测试用例。
首先,先明确几个核心问题的原因:
- 你之前触发原方法,大概率是因为没有正确替换路由中依赖的Detector类,或者是因为
getcontent是箭头函数,直接stub原型方法无效(箭头函数绑定在实例上,不是原型)。 - 模拟async/await其实很简单,只要让Sinon的stub返回一个Promise即可,Sinon专门提供了
resolves和rejects方法来处理这种场景。
先补全你的代码上下文
假设你的Detector类完整代码是这样的(补全了语法错误):
// Detector.js export default class Detector { constructor() {} setDetectorStrategy(detector) { this.detector = detector; } getcontent = async(payload) => { console.log('kkkkkkkkkkkkkkkkkkk...'); // 这里是实际的异步逻辑,比如调用API return '真实内容'; } }
路由文件(比如router.js)大概是这样:
// router.js import express from 'express'; import Detector from './Detector'; const router = express.Router(); const detectorInstance = new Detector(); // 假设这是你要测试的路由 router.post('/get-content', async (req, res) => { try { const content = await detectorInstance.getcontent(req.body); res.status(200).json({ success: true, content }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); export default router;
编写测试用例的步骤
我们会用到mocha(测试框架)、sinon(模拟工具)、proxyquire(替换模块依赖)、supertest(测试HTTP路由)、chai(断言库),先确保这些依赖都安装了。
1. 测试文件结构(detect-router.test.js)
const { expect } = require('chai'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); const supertest = require('supertest'); const express = require('express'); describe('Detector 路由测试', () => { let app; let detectorStub; // Detector类的stub let detectorInstanceStub; // Detector实例的stub // 每个测试用例前初始化stub和app beforeEach(() => { // 1. 创建实例的方法stub:因为getcontent是箭头函数,必须直接stub实例上的方法 detectorInstanceStub = { getcontent: sinon.stub(), setDetectorStrategy: sinon.stub() }; // 2. 创建Detector类的stub:调用new Detector()时返回我们的实例stub detectorStub = sinon.stub().returns(detectorInstanceStub); // 3. 用proxyquire加载路由,替换掉真实的Detector依赖 const router = proxyquire('./router', { './Detector': detectorStub }).default; // 4. 创建express app并挂载路由 app = express(); app.use(express.json()); // 解析JSON请求体 app.use(router); }); // 每个测试用例后清除stub的调用记录 afterEach(() => { sinon.restore(); }); // 测试成功场景:getcontent返回模拟内容 it('应该返回模拟的内容当getcontent成功时', async () => { // 模拟async方法的成功返回值 const mockContent = '我是模拟的内容'; detectorInstanceStub.getcontent.resolves(mockContent); // 发送POST请求到路由 const response = await supertest(app) .post('/get-content') .send({ key: 'test' }); // 断言响应结果 expect(response.status).to.equal(200); expect(response.body.success).to.be.true; expect(response.body.content).to.equal(mockContent); // 断言getcontent被正确调用,参数是请求体 expect(detectorInstanceStub.getcontent.calledOnceWith({ key: 'test' })).to.be.true; }); // 测试失败场景:getcontent抛出错误 it('应该返回错误当getcontent失败时', async () => { // 模拟async方法抛出错误 const mockError = new Error('获取内容失败'); detectorInstanceStub.getcontent.rejects(mockError); // 发送POST请求 const response = await supertest(app) .post('/get-content') .send({ key: 'test' }); // 断言响应结果 expect(response.status).to.equal(500); expect(response.body.success).to.be.false; expect(response.body.error).to.equal(mockError.message); // 断言getcontent被调用 expect(detectorInstanceStub.getcontent.calledOnce).to.be.true; }); // 测试setDetectorStrategy是否被调用(如果你的路由中有调用这个方法的逻辑) it('应该正确调用setDetectorStrategy方法', async () => { // 假设路由中初始化时调用了setDetectorStrategy expect(detectorStub.calledOnce).to.be.true; // 验证Detector类被实例化 // 如果你的路由中有 detectorInstance.setDetectorStrategy(xxx),可以加这个断言: // expect(detectorInstanceStub.setDetectorStrategy.calledOnce).to.be.true; }); });
关键知识点解释
为什么用proxyquire?
因为路由文件中直接import Detector并创建了实例,如果不替换这个依赖,测试时会一直用真实的Detector类,所以用proxyquire可以在加载路由时,把真实的Detector换成我们的stub类。如何模拟async/await函数?
async函数本质是返回Promise,所以用sinon.stub().resolves(模拟值)来模拟成功的Promise,用rejects(错误对象)来模拟失败的Promise,这样路由中的await就能拿到我们模拟的结果或者捕获错误。箭头函数的stub注意事项
你的getcontent是箭头函数,它是定义在实例上的属性,而不是原型方法,所以不能通过sinon.stub(Detector.prototype, 'getcontent')来模拟,必须直接stub实例上的方法,这也是我们用proxyquire返回实例stub的原因。
运行测试
在package.json中添加测试脚本:
{ "scripts": { "test": "mocha detect-router.test.js" } }
然后执行npm test就能看到测试结果了。
内容的提问来源于stack exchange,提问作者chrn

