使用Sinon单元测试Express.js路由遇阻,求解决方案
我完全理解你现在在Express路由单元测试上卡壳的感觉——新手阶段踩这种测试的坑太正常了!咱们一步步拆解问题,把这个测试给跑通。
首先先分析你当前测试报错的原因:你写的rootRouter.route('/').get(null, res)完全用错了Express的API!Route.get()是用来注册路由处理函数的,不是用来触发它执行的,你传个null当回调,Express当然会报错说“需要回调函数”。测试路由的正确姿势是拿到已经注册好的处理函数,给它传入模拟的req和res对象,然后断言res的方法有没有被正确调用。
第一步:先重构路由代码,让它更易测
你的路由里直接用了全局的host和port,这在测试环境里会因为没有赋值而报错,而且全局变量本身就不利于测试。咱们把它改成从req.app.locals取值(刚好你的server.js里也把DB连接挂在了app.locals上,风格统一):
// ./routers/root.js var express = require('express'); var router = express.Router(); // 把处理函数单独抽出来,方便直接测试 exports.getRootHandler = function(req, res) { // 从app.locals获取配置,测试时可以轻松注入测试值 const { host, port } = req.app.locals; res.writeHead(200, {'Content-Type': 'application/json'}); res.write(JSON.stringify({ Application: "NodeTours", Version: "2.0.0", Host: host, Port: port })); res.end(); }; router.route('/').get(exports.getRootHandler); module.exports = router;
第二步:写正确的单元测试代码
用Sinon模拟req、res对象,跟踪它们的方法调用,然后直接调用我们抽出来的处理函数,最后断言结果是否符合预期:
// ./test/test.js var sinon = require('sinon') var expect = require('chai').expect var { getRootHandler } = require('../routers/root') describe('Routers', function() { context('GET /', function() { let mockReq; let mockRes; // 每个测试前重置模拟对象,避免测试间的污染 beforeEach(function() { // 模拟req对象,注入测试用的host和port mockReq = { app: { locals: { host: 'test-localhost', port: '8888' } } }; // 用Sinon stub模拟res的所有方法,方便跟踪调用 mockRes = { writeHead: sinon.stub(), write: sinon.stub(), end: sinon.stub() }; }); it('should return 200 status and correct JSON response', function() { // 调用路由处理函数 getRootHandler(mockReq, mockRes); // 断言writeHead是否以正确参数被调用 expect(mockRes.writeHead.calledOnceWith( 200, {'Content-Type': 'application/json'} )).to.be.true; // 断言返回的JSON内容是否正确 const expectedResponseBody = JSON.stringify({ Application: "NodeTours", Version: "2.0.0", Host: 'test-localhost', Port: '8888' }); expect(mockRes.write.calledOnceWith(expectedResponseBody)).to.be.true; // 断言res.end()是否被调用 expect(mockRes.end.calledOnce).to.be.true; }); }); });
测试逻辑解释
- 模拟对象:用Sinon的
stub来模拟res的方法,这样我们能跟踪这些方法有没有被调用、传了什么参数。 - 直接调用处理函数:因为我们把处理函数单独导出了,不用再从路由栈里找,直接调用更简单。
- 断言验证:分别验证状态码、响应头、响应体、结束响应这几个关键步骤是否符合预期。
额外小建议
如果你不想重构路由代码,也可以从Express Router的stack数组里找到对应的处理函数(不过不如抽出来直观):
// 在测试里获取处理函数的方式 const getHandler = rootRouter.stack.find(layer => layer.route && layer.route.path === '/' && layer.route.methods.get ).handle;
内容的提问来源于stack exchange,提问作者Przemek
相关产品推荐
相关产品推荐

