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

Meteor单元测试:不依赖hwillson:stub-collections如何存根集合?

当然有办法!在Meteor框架里,就算受限于mongo@1.0.11版本没法用hwillson:stub-collections包,也不能用测试MongoDB实例,我们完全可以手动实现集合存根来完成单元测试。下面给你分享两种实用方案:

方案一:手动实现存根集合类

我们可以模拟Mongo.Collection的核心方法,创建一个纯内存的存根集合,所有操作都在内存中完成,完全不需要依赖MongoDB实例。

定义存根集合构造函数

import { Random } from 'meteor/random';

const StubCollection = function(collectionName) {
  this.name = collectionName;
  // 用数组存储模拟的文档
  this._documents = [];
};

// 模拟find方法,返回类似Meteor游标的对象
StubCollection.prototype.find = function(query = {}) {
  let filteredDocs = [...this._documents];
  
  // 简单处理_id匹配,可按需扩展其他查询条件
  if (query._id) {
    filteredDocs = filteredDocs.filter(doc => doc._id === query._id);
  }
  
  return {
    fetch: () => filteredDocs,
    count: () => filteredDocs.length,
    forEach: (callback) => filteredDocs.forEach(callback),
    // 按需添加其他需要的游标方法,比如map
    map: (fn) => filteredDocs.map(fn)
  };
};

// 模拟findOne方法
StubCollection.prototype.findOne = function(query = {}) {
  return this._documents.find(doc => {
    if (query._id) return doc._id === query._id;
    // 简单匹配多字段,可按需扩展复杂查询
    return Object.keys(query).every(key => doc[key] === query[key]);
  });
};

// 模拟insert方法,自动生成_id
StubCollection.prototype.insert = function(doc) {
  const newDoc = { ...doc, _id: Random.id() };
  this._documents.push(newDoc);
  return newDoc._id;
};

// 模拟update方法,支持$set操作符
StubCollection.prototype.update = function(query, modifier) {
  const docsToUpdate = this._documents.filter(doc => {
    if (query._id) return doc._id === query._id;
    return Object.keys(query).every(key => doc[key] === query[key]);
  });
  
  docsToUpdate.forEach(doc => {
    // 按需扩展其他操作符,比如$inc、$push
    if (modifier.$set) {
      Object.assign(doc, modifier.$set);
    }
  });
  
  return docsToUpdate.length;
};

// 模拟remove方法
StubCollection.prototype.remove = function(query) {
  const initialCount = this._documents.length;
  this._documents = this._documents.filter(doc => {
    if (query._id) return doc._id !== query._id;
    return !Object.keys(query).every(key => doc[key] === query[key]);
  });
  return initialCount - this._documents.length;
};

在测试中使用存根集合

// 假设你的业务集合定义在 imports/api/tasks/TasksCollection.js
import { TasksCollection } from '/imports/api/tasks/TasksCollection';

describe('Tasks业务逻辑测试', () => {
  let stubTasks;

  beforeEach(() => {
    // 创建存根集合实例
    stubTasks = new StubCollection('tasks');
    // 替换全局或模块中的原始集合
    global.TasksCollection = stubTasks;
    
    // 如果是模块导出的集合,可使用rewire工具替换:
    // const TasksModule = rewire('/imports/api/tasks/TasksCollection');
    // TasksModule.__set__('TasksCollection', stubTasks);
  });

  afterEach(() => {
    // 清理存根数据
    stubTasks._documents = [];
    // 恢复原始集合(如果需要)
    delete global.TasksCollection;
  });

  it('应该成功插入新任务', () => {
    const taskId = stubTasks.insert({ title: '写单元测试', completed: false });
    const insertedTask = stubTasks.findOne({ _id: taskId });
    
    expect(insertedTask.title).to.equal('写单元测试');
    expect(stubTasks.find().count()).to.equal(1);
  });

  it('应该更新任务的完成状态', () => {
    const taskId = stubTasks.insert({ title: '写单元测试', completed: false });
    stubTasks.update({ _id: taskId }, { $set: { completed: true } });
    
    const updatedTask = stubTasks.findOne({ _id: taskId });
    expect(updatedTask.completed).to.be.true;
  });
});
方案二:用Sinon.js存根Mongo.Collection原型方法

如果你的项目已经使用了Sinon.js,我们可以直接存根Mongo.Collection的原型方法,不用从头写整个存根类,更简洁高效。

import { Mongo } from 'meteor/mongo';
import { Random } from 'meteor/random';
import sinon from 'sinon';

// 假设你的业务集合
const TasksCollection = new Mongo.Collection('tasks');

describe('Tasks业务逻辑测试', () => {
  let findStub, findOneStub, insertStub;
  let testDocuments = [];

  beforeEach(() => {
    // 存根find方法
    findStub = sinon.stub(Mongo.Collection.prototype, 'find').callsFake(function(query) {
      let filtered = [...testDocuments];
      if (query._id) filtered = filtered.filter(doc => doc._id === query._id);
      return {
        fetch: () => filtered,
        count: () => filtered.length
      };
    });

    // 存根findOne方法
    findOneStub = sinon.stub(Mongo.Collection.prototype, 'findOne').callsFake(function(query) {
      return testDocuments.find(doc => {
        if (query._id) return doc._id === query._id;
        return Object.keys(query).every(key => doc[key] === query[key]);
      });
    });

    // 存根insert方法
    insertStub = sinon.stub(Mongo.Collection.prototype, 'insert').callsFake(function(doc) {
      const newDoc = { ...doc, _id: Random.id() };
      testDocuments.push(newDoc);
      return newDoc._id;
    });

    // 按需存根update、remove等方法
  });

  afterEach(() => {
    // 恢复所有Sinon存根
    findStub.restore();
    findOneStub.restore();
    insertStub.restore();
    // 清空测试数据
    testDocuments = [];
  });

  it('应该正确获取所有未完成任务', () => {
    testDocuments.push(
      { _id: '1', title: '任务1', completed: false },
      { _id: '2', title: '任务2', completed: true }
    );
    
    const incompleteTasks = TasksCollection.find({ completed: false }).fetch();
    expect(incompleteTasks.length).to.equal(1);
    expect(incompleteTasks[0].title).to.equal('任务1');
  });
});
注意事项
  • 按需扩展存根逻辑:根据你的测试场景,补充对复杂查询条件(如$gte、$in)或操作符(如$inc、$push)的支持。
  • 处理反应式:如果代码依赖Meteor的Tracker反应性,可在存根方法修改数据后调用Tracker.flush()触发反应更新。
  • 模块替换:如果集合通过ES模块导出,建议使用rewire或proxyquire工具替换模块内的集合实例,确保测试代码使用存根版本。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:53:53