You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:07:37