嘲笑/ st Mong模式保存方法

给定一个简单的mongoose模型:

import mongoose, { Schema } from 'mongoose'; const PostSchema = Schema({ title: { type: String }, postDate: { type: Date, default: Date.now } }, { timestamps: true }); const Post = mongoose.model('Post', PostSchema); export default Post; 

我想testing这个模型,但是我遇到了一些障碍。

我目前的规格看起来像这样(为了简洁,省略了一些东西):

 import mongoose from 'mongoose'; import { expect } from 'chai'; import { Post } from '../../app/models'; describe('Post', () => { beforeEach((done) => { mongoose.connect('mongodb://localhost/node-test'); done(); }); describe('Given a valid post', () => { it('should create the post', (done) => { const post = new Post({ title: 'My test post', postDate: Date.now() }); post.save((err, doc) => { expect(doc.title).to.equal(post.title) expect(doc.postDate).to.equal(post.postDate); done(); }); }); }); }); 

但是,每次我运行testing时,我都会打到我的数据库,我宁愿避免。

我试过使用Mockgoose ,但是然后我的testing不会运行。

 import mockgoose from 'mockgoose'; // in before or beforeEach mockgoose(mongoose); 

testing卡住,并抛出一个错误说: Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. 我试过把超时时间增加到20秒,但是没有解决任何问题。

接下来,我扔掉了Mockgoose,并试图用Sinon来save电话。

 describe('Given a valid post', () => { it('should create the post', (done) => { const post = new Post({ title: 'My test post', postDate: Date.now() }); const stub = sinon.stub(post, 'save', function(cb) { cb(null) }) post.save((err, post) => { expect(stub).to.have.been.called; done(); }); }); }); 

这个testing通过了,但它对我来说没什么意义。 我很蠢,嘲笑,你有什么,…我不知道这是否是正确的路要走。 我在save post save方法,然后我声称它已被调用,但我明显地调用它…此外,我似乎无法得到的参数非截尾mongoose方法会返回。 我想将postvariables与save方法返回的内容进行比较,就像我在第一次testing数据库的时候一样。 我已经尝试了几种 方法,但都感觉很不方便。 必须有一个干净的方式,不是吗?

几个问题:

  • 我是否应该避免像往常一样阅读数据库? 我的第一个例子工作正常,我可以在每次运行后清除数据库。 但是,这对我来说并不合适。

  • 我将如何存储从Mongoose模型的保存方法,并确保它实际上testing我想testing:保存一个新的对象到数据库。

基础

在unit testing中,不应该碰到数据库。 我可以想到一个例外:打到一个内存数据库,但即使这样也已经存在于集成testing领域,因为您只需要在内存中保存复杂进程的状态(因此不是真正的function单元)。 所以,没有实际的数据库。

你想在unit testing中testing的是你的业务逻辑在你的应用程序和数据库之间的接口上产生正确的API调用。 您可以也可能应该假设数据库API /驱动程序开发人员已经做了很好的testing,发现API下的所有东西都像预期的那样工作。 但是,您还希望在testing中涵盖您的业务逻辑如何对不同的有效API结果作出反应,如成功保存,由于数据一致性导致的失败,由于连接问题导致的失败等。

这意味着您需要和想要模拟的是DB驱动程序界面下的所有内容。 但是,您需要对此行为进行build模,以便您的业务逻辑可以针对数据库调用的所有结果进行testing。

说起来容易做起来难,因为这意味着你需要通过你使用的技术来访问API,你需要知道API。

mongoose的现实

坚持基本的原则,我们想嘲笑mongoose使用的潜在的“驱动程序”执行的调用。 假设它是node-mongodb-native,我们需要嘲笑这些调用。 理解mongoose与本地驱动程序之间的完全相互作用并不容易,但一般归结为mongoose.Collection的方法。因为后者扩展了mongoldb.Collection并且不像insert 那样实现方法。 如果我们能够在这个特定情况下控制insert的行为,那么我们知道我们在API级别上嘲笑了数据库访问。 您可以在两个项目的源代码中追踪它,该Collection.insert实际上是本机驱动程序方法。

对于您的特定示例,我创build了一个完整的包的公共Git存储库 ,但是我将在此处发布所有元素。

解决scheme

就我个人而言,我发现使用mongoose的“推荐”方法是非常不可用的:模型通常在定义相应模式的模块中创build,但它们已经需要连接。 为了让多个连接在同一个项目中与完全不同的mongodb数据库进行通信并进行testing,这使得生活变得非常困难。 事实上,只要担心完全分离,mongoose至less对我来说几乎是无法使用的。

所以我创build的第一件事就是包描述文件,一个包含模式的模块和一个通用的“模型生成器”:

的package.json

 { "name": "xxx", "version": "0.1.0", "private": true, "main": "./src", "scripts": { "test" : "mocha --recursive" }, "dependencies": { "mongoose": "*" }, "devDependencies": { "mocha": "*", "chai": "*" } } 

SRC / post.js

 var mongoose = require("mongoose"); var PostSchema = new mongoose.Schema({ title: { type: String }, postDate: { type: Date, default: Date.now } }, { timestamps: true }); module.exports = PostSchema; 

SRC / index.js

 var model = function(conn, schema, name) { var res = conn.models[name]; return res || conn.model.bind(conn)(name, schema); }; module.exports = { PostSchema: require("./post"), model: model }; 

这样的模型生成器有其缺点:有些元素可能需要连接到模型,将它们放置在创build模式的相同模块中是有意义的。 所以find一个通用的方式来添加这些是有点棘手。 例如,一个模块可以导出后续操作,当为给定的连接等(黑客)生成模型时自动运行。

现在我们来嘲笑这个API。 我会保持简单,只会嘲笑我所需要的testing。 一般来说,我很想嘲笑API,而不是个别实例的个别方法。 后者可能在某些情况下是有用的,或者什么也没有帮助,但是我需要访问在我的业务逻辑中创build的对象(除非通过某种工厂模式注入或提供),这将意味着修改主要来源。 同时,在一个地方嘲笑API有一个缺点:它是一个通用的解决scheme,可能会实现成功的执行。 为了testing错误情况,可能需要在testing本身中模拟实例,但是在业务逻辑中,您可能无法直接访问例如在内部创build的post的实例。

那么,让我们来看看嘲笑成功的API调用的一般情况:

testing/ mock.js

 var mongoose = require("mongoose"); // this method is propagated from node-mongodb-native mongoose.Collection.prototype.insert = function(docs, options, callback) { // this is what the API would do if the save succeeds! callback(null, docs); }; module.exports = mongoose; 

一般来说,只要模型是修改mongoose之后创build的,就可以想象上面的模拟是在每个testing的基础上完成的,以模拟任何行为。 确保恢复到原来的行为,但是,在每次testing之前!

最后这就是我们如何testing所有可能的数据保存操作。 请注意,这些不是特定于我们的Post模型,并且可以针对所有其他具有完全相同模拟的模型完成。

testing/ test_model.js

 // now we have mongoose with the mocked API // but it is essential that our models are created AFTER // the API was mocked, not in the main source! var mongoose = require("./mock"), assert = require("assert"); var underTest = require("../src"); describe("Post", function() { var Post; beforeEach(function(done) { var conn = mongoose.createConnection(); Post = underTest.model(conn, underTest.PostSchema, "Post"); done(); }); it("given valid data post.save returns saved document", function(done) { var post = new Post({ title: 'My test post', postDate: Date.now() }); post.save(function(err, doc) { assert.deepEqual(doc, post); done(err); }); }); it("given valid data Post.create returns saved documents", function(done) { var post = new Post({ title: 'My test post', postDate: 876543 }); var posts = [ post ]; Post.create(posts, function(err, docs) { try { assert.equal(1, docs.length); var doc = docs[0]; assert.equal(post.title, doc.title); assert.equal(post.date, doc.date); assert.ok(doc._id); assert.ok(doc.createdAt); assert.ok(doc.updatedAt); } catch (ex) { err = ex; } done(err); }); }); it("Post.create filters out invalid data", function(done) { var post = new Post({ foo: 'Some foo string', postDate: 876543 }); var posts = [ post ]; Post.create(posts, function(err, docs) { try { assert.equal(1, docs.length); var doc = docs[0]; assert.equal(undefined, doc.title); assert.equal(undefined, doc.foo); assert.equal(post.date, doc.date); assert.ok(doc._id); assert.ok(doc.createdAt); assert.ok(doc.updatedAt); } catch (ex) { err = ex; } done(err); }); }); }); 

需要注意的是,我们仍然在testing非常低级别的function,但我们可以使用相同的方法来testing任何使用Post.createpost.save内部的业务逻辑。

最后一点,让我们运行testing:

〜/ source / web / xxx $ npmtesting

 > xxx@0.1.0 test /Users/osklyar/source/web/xxx > mocha --recursive Post ✓ given valid data post.save returns saved document ✓ given valid data Post.create returns saved documents ✓ Post.create filters out invalid data 3 passing (52ms) 

我必须说,这样做并不好玩。 但是,这种方法对于业务逻辑来说确实是纯粹的unit testing,没有任何内存或真正的数据库,而且相当通用。

如果你想要的是testing某些Mongoose模型的static'smethod's ,我build议你使用sinon和sinon-mongoose 。 (我猜这是与柴兼容)

这样,你将不需要连接到Mongo DB。

按照你的例子,假设你有一个静态方法findLast

 //If you are using callbacks PostSchema.static('findLast', function (n, callback) { this.find().limit(n).sort('-postDate').exec(callback); }); //If you are using Promises PostSchema.static('findLast', function (n) { this.find().limit(n).sort('-postDate').exec(); }); 

然后,testing这个方法

 var Post = mongoose.model('Post'); // If you are using callbacks, use yields so your callback will be called sinon.mock(Post) .expects('find') .chain('limit').withArgs(10) .chain('sort').withArgs('-postDate') .chain('exec') .yields(null, 'SUCCESS!'); Post.findLast(10, function (err, res) { assert(res, 'SUCCESS!'); }); // If you are using Promises, use 'resolves' (using sinon-as-promised npm) sinon.mock(Post) .expects('find') .chain('limit').withArgs(10) .chain('sort').withArgs('-postDate') .chain('exec') .resolves('SUCCESS!'); Post.findLast(10).then(function (res) { assert(res, 'SUCCESS!'); }); 

你可以find有关sinon-mongoose回购的工作(和简单的)例子。