基于Node-Express-Mongoose的MVC API单元测试实现方法咨询
针对你的Node-Express-Mongoose MVC项目的单元测试指南(Mocha + Chai + Sinon)
嘿,作为单元测试新手,针对你这个基于Node-Express-Mongoose的MVC项目,咱们一步步拆解怎么写单元测试,还有Sinon到底要不要用——答案是肯定的,后面我会讲清楚原因。
首先得明确单元测试的核心:隔离被测模块,只测它自身的逻辑,外部依赖(比如数据库、其他工具函数)都要模拟掉,这样测试才快、稳定,而且能精准覆盖所有代码分支。
1. 先装依赖
先把需要的测试工具装到开发依赖里:
npm install mocha chai sinon sinon-chai supertest --save-dev
mocha:负责跑测试的运行器chai:断言库,我个人推荐用expect风格,写起来更自然sinon:用来创建存根(stub)、模拟(mock),帮你把外部依赖给“架空”,避免真操作数据库sinon-chai:让Sinon的断言和Chai兼容,写法更统一supertest:可选,但用来模拟HTTP请求测试Express控制器会更方便(不过咱们这里用模拟req/res的方式也能搞定)
2. 先测util.js
util.js的核心是调用Mongoose的comments.find(),所以咱们要模拟comments模型,绝对不能真的连数据库——不然测试结果全看数据库里有没有数据,太不稳定了。
写测试文件:util.test.js
const { expect } = require('chai'); const sinon = require('sinon'); const commentsModel = require('../models/comments'); const util = require('../util'); describe('util.getComments', () => { let findStub; // 每个测试前重置存根,避免测试互相影响 beforeEach(() => { // 把comments.find方法换成我们的存根 findStub = sinon.stub(commentsModel, 'find'); // 因为Mongoose是链式调用(find之后要sort),所以得模拟sort方法的返回 findStub.returns({ sort: sinon.stub().resolves([{ email: 'test@example.com', content: 'test comment' }]) }); }); // 每个测试后恢复原方法,别影响其他代码 afterEach(() => { findStub.restore(); }); it('应该用传入的查询条件调用comments.find,并按createdAt倒序排序', async () => { const testQuery = { email: /test/i }; await util.getComments(testQuery); // 断言find方法被调用了一次,参数是我们传入的testQuery expect(findStub.calledOnceWith(testQuery)).to.be.true; // 断言sort方法被调用了一次,参数是{ createdAt: -1 } expect(findStub.returnValues[0].sort.calledOnceWith({ createdAt: -1 })).to.be.true; }); it('当comments.find失败时,应该抛出对应的错误', async () => { const testError = new Error('数据库连接失败'); // 修改存根,让sort方法抛出错误 findStub.returns({ sort: sinon.stub().rejects(testError) }); try { await util.getComments({}); // 如果没抛出错误,测试直接失败 expect.fail('应该抛出错误,但没抛'); } catch (err) { expect(err).to.equal(testError); } }); });
3. 再测controller.js
控制器的逻辑是:处理请求参数→调用util.getComments→返回响应。这里要模拟util.getComments,还要模拟Express的req和res对象,不用真的启动服务器。
写测试文件:controller.test.js
const { expect } = require('chai'); const sinon = require('sinon'); const controller = require('../controller'); const util = require('../util'); describe('controller.getComments', () => { let getCommentsStub; let req; let res; beforeEach(() => { // 把util.getComments换成我们的存根 getCommentsStub = sinon.stub(util, 'getComments'); // 模拟Express的req和res对象 req = { query: {} }; res = { json: sinon.spy(), // 用spy来跟踪这个方法有没有被调用 status: sinon.stub().returnsThis(), // 模拟链式调用,status返回res本身 send: sinon.spy() }; }); afterEach(() => { getCommentsStub.restore(); }); it('当没有传入filter参数时,应该返回所有评论', async () => { const mockComments = [{ email: 'a@test.com' }, { email: 'b@test.com' }]; getCommentsStub.resolves(mockComments); await controller.getComments(req, res); // 断言util.getComments被调用了一次,参数是空对象 expect(getCommentsStub.calledOnceWith({})).to.be.true; // 断言res.json被调用了一次,参数是模拟的评论数据 expect(res.json.calledOnceWith(mockComments)).to.be.true; }); it('当传入filter参数时,应该按邮箱模糊过滤评论', async () => { req.query.filter = 'test'; const expectedQuery = { email: new RegExp('.*test.*', 'i') }; const mockFilteredComments = [{ email: 'test@example.com' }]; getCommentsStub.resolves(mockFilteredComments); await controller.getComments(req, res); expect(getCommentsStub.calledOnceWith(expectedQuery)).to.be.true; expect(res.json.calledOnceWith(mockFilteredComments)).to.be.true; }); it('当util.getComments抛出错误时,应该返回500状态和错误信息', async () => { const testError = new Error('出问题啦'); getCommentsStub.rejects(testError); await controller.getComments(req, res); expect(res.status.calledOnceWith(500)).to.be.true; expect(res.send.calledOnceWith(`Internal server error: ${testError}`)).to.be.true; }); });
4. 为什么一定要用Sinon?
简单说:
- 隔离依赖:如果不用Sinon,你的测试会真的连数据库、调用真实的工具函数,这样测试速度慢,还依赖外部环境(比如数据库里有没有数据),测试结果根本不稳定。
- 覆盖所有分支:Sinon能让你轻松模拟各种场景——比如让工具函数返回成功数据、抛出错误,这样你就能覆盖控制器里的try/catch分支,还有util里的错误处理。
- 简化代码:手动模拟依赖的话,你得写一堆假对象,容易出错,用Sinon的stub、spy能省很多事,代码也更清晰。
5. 跑测试
在package.json里加个测试脚本:
{ "scripts": { "test": "mocha **/*.test.js" } }
然后运行:
npm test
内容的提问来源于stack exchange,提问作者wizard
相关产品推荐
相关产品推荐

