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
相关产品推荐
相关产品推荐

