如何为SailsJS编写单元测试?TDD流程与依赖解耦问题
在SailsJS中实现控制器的TDD工作流:解耦与模拟指南
我完全懂你的困扰——从C#/Java那种依赖注入友好的环境转去SailsJS做TDD,确实会被框架的“隐式绑定”特性绊住脚。下面我会一步步帮你解决如何加载控制器到Mocha测试、解耦数据库依赖,以及明确需要模拟的部分:
一、先把控制器逻辑从请求/响应中抽离(核心第一步)
SailsJS的控制器默认和req/res绑定,但TDD的关键是测试业务逻辑而非请求处理。你可以先把核心逻辑抽成独立的可调用函数,避免直接依赖框架的请求对象:
修改你的控制器代码:
// api/controllers/StudentController.js const request = require('request'); module.exports = { // 对外的请求处理入口 updateData: async function (req, res) { try { const idx = req.params.jobNumber; const result = await this._processStudentUpdate(idx); res.ok(result); } catch (err) { res.serverError(err); } }, // 抽离的核心业务逻辑(可直接测试) _processStudentUpdate: async function(jobNumber) { const student = await Students.find({Nr: jobNumber}); // 这里放你的业务处理逻辑,比如数据转换、校验等 return student; } };
这样在Mocha中,你可以直接加载控制器并调用_processStudentUpdate,无需模拟完整的req/res(当然如果要测请求处理逻辑,也可以模拟这两个对象)。
二、用模拟工具替换Sails的数据库模型
SailsJS的模型是全局隐式加载的,这是耦合的根源。我们可以用rewire(模块替换工具)和sinon(测试模拟库)来替换控制器中的Students模型,避免连接真实数据库:
1. 安装依赖
npm install mocha sinon rewire chai --save-dev
2. 编写Mocha测试用例
// test/unit/controllers/StudentController.test.js const rewire = require('rewire'); const sinon = require('sinon'); const chai = require('chai'); const expect = chai.expect; // 加载控制器并启用rewire的替换功能 const studentController = rewire('../../api/controllers/StudentController'); describe('StudentController', () => { let mockStudents; // 每个测试前重置模拟对象 beforeEach(() => { // 模拟Students模型的find方法(适配Sails的promise/回调两种写法) mockStudents = { find: sinon.stub().resolves([{Nr: '123', name: 'Paul'}]) }; // 替换控制器中的全局Students依赖 studentController.__set__('Students', mockStudents); }); it('should fetch student by job number', async () => { // 直接测试抽离的核心逻辑 const result = await studentController._processStudentUpdate('123'); // 验证结果是否符合预期 expect(result).to.deep.equal([{Nr: '123', name: 'Paul'}]); // 验证模型方法是否被正确调用 sinon.assert.calledWith(mockStudents.find, {Nr: '123'}); }); it('should handle request and return success response', async () => { // 模拟req和res对象 const req = { params: { jobNumber: '123' } }; const res = { ok: sinon.stub(), serverError: sinon.stub() }; await studentController.updateData(req, res); // 验证res.ok是否被调用,且参数正确 sinon.assert.calledWith(res.ok, [{Nr: '123', name: 'Paul'}]); // 验证res.serverError没有被触发 sinon.assert.notCalled(res.serverError); }); });
三、明确需要模拟的核心部分
你不需要模拟整个Sails框架,只需要替换控制器直接依赖的外部资源:
- Sails模型(如Students):这是最关键的部分,模拟模型的所有数据库操作方法(
find/create/update等,取决于你的控制器用到哪些),确保测试不触达真实数据库。 - 请求/响应对象(req/res):如果要测试完整的控制器请求处理逻辑,需要模拟
req的params/body等属性,以及res的响应方法(ok/serverError/badRequest等),验证这些方法是否被正确调用。 - 其他外部依赖:比如你代码中的
request模块,如果控制器里用它发起HTTP请求,也要模拟request的方法,避免发起真实网络请求。
四、长期解耦的最佳实践:用Service层分离业务逻辑
为了避免控制器和框架/数据库过度耦合,建议把业务逻辑放到Sails的api/services目录下,让控制器只做请求转发:
// api/services/StudentService.js module.exports = { async getStudentByJobNumber(jobNumber) { const student = await Students.find({Nr: jobNumber}); // 这里集中处理所有业务逻辑 return student; } };
然后控制器简化为:
// api/controllers/StudentController.js const StudentService = require('../services/StudentService'); module.exports = { updateData: async function (req, res) { try { const result = await StudentService.getStudentByJobNumber(req.params.jobNumber); res.ok(result); } catch (err) { res.serverError(err); } } };
这样测试的重心就转移到Service层,控制器的测试会非常轻量化——只需要验证它正确调用了Service和响应方法即可,Service层的测试则和之前的逻辑一致,模拟模型依赖即可。
内容的提问来源于stack exchange,提问作者paul23
相关产品推荐
相关产品推荐

