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

如何用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();
            }
        )
});

关键说明

  1. returnsThis():让skip方法返回queryFake自身,保证链式调用可以继续执行
  2. limit方法直接resolve模拟数据,适配await的Promise等待逻辑
  3. 必须模拟countDocuments:否则服务执行到该步骤会调用真实方法或报错
  4. 调整断言结构:服务返回的是{data, total},所以要对应修改断言路径,之前的res.body.data[randomModel.name]是错误的

内容的提问来源于stack exchange,提问作者Miguel V

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 13:15:51