如何用Sinon Fakes测试Mongoose方法?解决skip/limit调用报错
解决Mongoose链式查询的Sinon模拟问题
问题背景
我用Sinon、Mocha和Chai搭建了动态模型查询接口的测试框架,接口会根据路由匹配对应的Mongoose模型执行带分页(skip/limit)的查询。但运行测试时抛出错误,移除skip()和limit()后测试正常,需要正确模拟这些链式调用方法。
测试框架代码
type ModelObject = { name: string; model: typeof Categoria | typeof Articulo | typeof Usuario; fakeMultiple: () => object[]; fakeOne: (id?: string) => object; } const models: ModelObject[] = [ { name: 'categorias', model: Categoria, fakeMultiple: () => fakeMultiple({ creator: oneCategoria }), fakeOne: oneCategoria }, { name: 'articulos', model: Articulo, fakeMultiple: () => fakeMultiple({ creator: oneArticulo }), fakeOne: oneArticulo }, { name: 'usuarios', model: Usuario, fakeMultiple: () => fakeMultiple({ creator: oneUsuario }), fakeOne: oneUsuario } ]; const randomModel = models[Math.floor(Math.random() * models.length)]; describe(`v1/${randomModel.name}`, function () { this.afterEach(function () { sinon.restore(); }); context.only("When requesting information from an endpoint, this should take the Model of the requested endpoint and query the database for all the elements of that model", function () { it.only(`Should return a list of elements of ${randomModel.name} model`, function (done) { const fakes = randomModel.fakeMultiple(); const findFake = sinon.fake.resolves({ [randomModel.name]: fakes }); sinon.replace(randomModel.model, 'find', findFake); chai.request(app) .get(`/api/v1/${randomModel.name}`) .end( (err, res) => { expect(res).to.have.status(200); expect(res.body.data).to.be.an('object'); expect(res.body.data).to.have.property(randomModel.name); expect(res.body.data[randomModel.name]).to.have.lengthOf(fakes.length); expect(findFake.calledOnce).to.be.true; done(); } ) }); }}
查询服务实现代码
import { Articulo } from '../models/articulo'; import { Usuario } from '../models/usuario'; import { Categoria } from '../models/categoria'; import logger from '../config/logging'; import { Model } from 'mongoose'; const determineModel = (model: string): Model<any> => { switch (model) { case 'articulos': return Articulo; case 'usuarios': return Usuario; case 'categorias': return Categoria; default: throw new Error(`Model ${model} not found`); } }; export const getInformation = async (schema: string, page: number, limit: number) => { try { const model = determineModel(schema); const data = await model.find().skip((page - 1) * limit).limit(limit); const dataLength = await model.find().countDocuments(); return { data, total: dataLength, }; } catch (err) { logger.error(err); console.log(err); throw err; } };
运行错误信息
error: model.find(...).skip is not a function
TypeError: model.find(...).skip is not a function
解决方案
问题核心是你只模拟了find方法,但Mongoose的查询是链式调用——find返回的是Query对象,需要模拟这个对象的skip、limit方法,且这些方法要返回自身维持链式,最后limit要返回Promise(因为用了await)。同时还要模拟countDocuments方法,因为服务里也调用了它。
修改后的测试用例代码
it.only(`Should return a list of elements of ${randomModel.name} model`, function (done) { const fakes = randomModel.fakeMultiple(); const defaultPage = 1; const defaultLimit = 15; // 模拟链式调用的Query对象 const queryFake = { skip: sinon.fake.returnsThis(), // 返回自身维持链式 limit: sinon.fake.resolves(fakes) // 最终返回模拟数据 }; const findFake = sinon.fake.returns(queryFake); const countFake = sinon.fake.resolves(fakes.length); sinon.replace(randomModel.model, 'find', findFake); sinon.replace(randomModel.model, 'countDocuments', countFake); chai.request(app) .get(`/api/v1/${randomModel.name}`) .end( (err, res) => { expect(res).to.have.status(200); expect(res.body.data).to.be.an('object'); expect(res.body.data).to.have.property('data'); expect(res.body.data.data).to.have.lengthOf(fakes.length); expect(res.body.data.total).to.equal(fakes.length); // 验证各方法的调用情况 expect(findFake.calledOnce).to.be.true; expect(queryFake.skip.calledOnceWith((defaultPage - 1) * defaultLimit)).to.be.true; expect(queryFake.limit.calledOnceWith(defaultLimit)).to.be.true; expect(countFake.calledOnce).to.be.true; done(); } ) });
关键说明
returnsThis():让skip方法返回queryFake自身,保证链式调用可以继续执行limit方法直接resolve模拟数据,适配await的Promise等待逻辑- 必须模拟
countDocuments:否则服务执行到该步骤会调用真实方法或报错 - 调整断言结构:服务返回的是
{data, total},所以要对应修改断言路径,之前的res.body.data[randomModel.name]是错误的
内容的提问来源于stack exchange,提问作者Miguel V
相关产品推荐
相关产品推荐

