cab*_*ret 15 unit-testing mongoose mongodb node.js sinon
给出一个简单的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;
Run Code Online (Sandbox Code Playgroud)
我希望测试这个模型,但我遇到了一些障碍.
我当前的规范看起来像这样(为简洁起见省略了一些东西):
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();
});
});
});
});
Run Code Online (Sandbox Code Playgroud)
然而,与此我每次运行测试,我宁愿避免时间打我的数据库.
我尝试过使用Mockgoose,但我的测试不会运行.
import mockgoose from 'mockgoose';
// in before or beforeEach
mockgoose(mongoose);
Run Code Online (Sandbox Code Playgroud)
测试卡,并抛出一个错误说: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();
});
});
});
Run Code Online (Sandbox Code Playgroud)
这个测试通过了,但它对我来说不太合理.我很陌生,嘲笑,你有什么,......我不确定这是不是正确的方法.我正在对save
方法进行存根post
,然后我断言它已被调用,但我显然是在调用它...而且,我似乎无法得到非存根Mongoose方法将返回的参数.我想将post
变量与save
方法返回的内容进行比较,就像我在第一次测试数据时所做的那样.我试了几个的方法,但他们都感到相当的hackish.必须有一个干净的方式,不是吗?
几个问题:
我是否真的应该避免像往常一样阅读数据库?我的第一个例子运行正常,我可以在每次运行后清除数据库.但是,它对我来说并不合适.
我如何从Mongoose模型中存储save方法,并确保它实际测试我想要测试的内容:将新对象保存到db.
Ole*_*yar 43
基础
在单元测试中,不应该打到DB.我可以想到一个例外:命中内存数据库,但即使这已经存在于集成测试领域,因为你只需要在内存中保存的状态用于复杂的进程(因此不是真正的功能单元).所以,是的,没有实际的DB.
您希望在单元测试中测试的是,您的业务逻辑会在应用程序和数据库之间的接口处产生正确的API调用.您可以并且可能应该假设数据库API /驱动程序开发人员已经很好地测试了API下面的所有内容都按预期运行.但是,您还希望在测试中涵盖业务逻辑如何对不同的有效API结果做出反应,例如成功保存,由于数据一致性导致的故障,由于连接问题导致的故障等.
这意味着您需要和想要模拟的是DB驱动程序接口之下的所有内容.但是,您需要对该行为进行建模,以便可以针对数据库调用的所有结果测试您的业务逻辑.
说起来容易做起来难,因为这意味着您需要通过您使用的技术访问API,并且您需要了解API.
猫鼬的现实
坚持基础,我们想模拟由猫鼬使用的底层"驱动程序"执行的调用.假设它是node-mongodb-native,我们需要模拟这些调用.理解mongoose和本机驱动程序之间的完全相互作用并不容易,但它通常归结为方法,mongoose.Collection
因为后者扩展mongoldb.Collection
并且不重新实现类似的方法insert
.如果我们能够控制insert
在这种特定情况下的行为,那么我们就知道我们在API级别模拟了数据库访问.您可以在两个项目的源代码中跟踪它,这Collection.insert
实际上是本机驱动程序方法.
对于您的特定示例,我创建了一个包含完整包的公共Git存储库,但我将在答案中发布所有元素.
解决方案
就个人而言,我发现使用mongoose的"推荐"方式非常不可用:模型通常在模块中创建,其中定义了相应的模式,但它们已经需要连接.为了在同一个项目中有多个连接与完全不同的mongodb数据库进行通信,并且出于测试目的,这使得生活变得非常困难.事实上,只要问题完全分开,至少对我而言,猫鼬几乎无法使用.
所以我创建的第一件事是包描述文件,一个带有模式的模块和一个通用的"模型生成器":
的package.json
{
"name": "xxx",
"version": "0.1.0",
"private": true,
"main": "./src",
"scripts": {
"test" : "mocha --recursive"
},
"dependencies": {
"mongoose": "*"
},
"devDependencies": {
"mocha": "*",
"chai": "*"
}
}
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
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
};
Run Code Online (Sandbox Code Playgroud)
这样的模型生成器有它的缺点:有些元素可能需要附加到模型上,将它们放在创建模式的同一模块中是有意义的.因此,找到一种添加它们的通用方法有点棘手.例如,模块可以导出在为给定连接等生成模型时自动运行的后操作(黑客攻击).
现在让我们嘲笑API.我会保持简单,只会嘲笑我所需要的测试.我一般要模拟API,而不是个别实例的单独方法.后者在某些情况下可能很有用,或者在没有其他帮助的情况下,但我需要访问在我的业务逻辑中创建的对象(除非通过某些工厂模式注入或提供),这将意味着修改主要源.同时,在一个地方模拟API有一个缺点:它是一个通用的解决方案,可能会成功执行.对于测试错误情况,可能需要在测试中自己进行模拟,但是在您的业务逻辑中,您可能无法直接访问例如post
内部创建的实例.
那么,让我们看一下模拟成功API调用的一般情况:
测试/ 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;
Run Code Online (Sandbox Code Playgroud)
通常,只要在修改mongoose 之后创建模型,就可以想到上面的模拟是在每个测试的基础上完成的,以模拟任何行为.但是,确保在每次测试之前恢复原始行为!
最后,这是我们对所有可能的数据保存操作的测试看起来如何.请注意,这些并非特定于我们的Post
模型,并且可以在具有完全相同模拟的所有其他模型上完成.
测试/ 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);
});
});
});
Run Code Online (Sandbox Code Playgroud)
值得注意的是,我们仍在测试非常低级别的功能,但我们可以使用相同的方法来测试使用Post.create
或post.save
内部使用的任何业务逻辑.
最后一点,让我们运行测试:
〜/ source/web/xxx $ npm test
> 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)
Run Code Online (Sandbox Code Playgroud)
我必须说,这样做并不好玩.但是这样,它实际上是对业务逻辑的纯单元测试,没有任何内存或真实数据库,而且相当通用.
如果你想要的是测试static's
和method's
某些Mongoose模型,我建议你使用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();
});
Run Code Online (Sandbox Code Playgroud)
然后,测试这个方法
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!');
});
Run Code Online (Sandbox Code Playgroud)
你可以在sinon-mongoose repo 上找到工作(和简单)的例子.
归档时间: |
|
查看次数: |
11379 次 |
最近记录: |