基础知识
在单元测试中,不应该对数据库进行访问。我能想到一个例外情况:对内存中的数据库进行访问,但即使这样也已经属于集成测试范畴,因为您仅需要为复杂进程保存在内存中的状态(因此不是真正的功能单元)。所以,是的,没有实际的数据库。
在单元测试中要测试的内容是,您的业务逻辑是否导致正确的API调用在应用程序和数据库之间的接口处。您可以并且可能应该假设DB API /驱动程序开发人员已经很好地测试了API下方所有内容的行为与预期一致。然而,在您的测试中,您还希望涵盖您的业务逻辑如何针对不同的有效API结果(例如成功的保存,由于数据一致性而失败,由于连接问题而失败等)做出反应。
这意味着您需要模拟的是DB驱动程序接口以下的所有内容。但是,您需要建模其行为,以便可以对DB调用的所有结果测试您的业务逻辑。
说起来容易做起来难,因为这意味着您需要通过使用的技术访问API,并且您需要了解API。
Mongoose的实际情况
坚持基本原则,我们想模拟mongoose使用的底层“驱动程序”执行的调用。假设它是node-mongodb-native,我们需要模拟出这些调用。理解mongoose和本机驱动程序之间的完整相互作用并不容易,但通常归结于mongoose.Collection
中的方法,因为后者扩展了mongoldb.Collection
,并且不重新实现像insert
这样的方法。如果我们能够在这种特殊情况下控制insert
的行为,则我们知道我们已经在API级别上模拟了DB访问。您可以在两个项目的源代码中跟踪它,Collection.insert
确实是本机驱动程序方法。
对于您的特定示例,我创建了一个包含完整包的公共Git存储库,但我将在答案中发布所有元素。
解决方案
个人认为与mongoose配合使用的“推荐”方式非常难以使用:模型通常在定义相应架构的模块中创建,但它们已经需要连接。为了在同一项目中与完全不同的mongodb数据库进行多个连接,并用于测试目的,这使得生活非常困难。事实上,只要关注点被完全分开,对我来说mongoose几乎无法使用。
因此,我首先创建的是包描述文件、一个包含模式和通用“模型生成器”的模块:
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;
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
};
这样的模型生成器存在一些缺点:可能需要将某些元素附加到模型上,并且将它们放置在创建模式的同一模块中是有意义的。因此,找到一种通用的方法来添加它们有点棘手。例如,一个模块可以导出 post-actions,在为给定连接生成模型时自动运行它们等(黑客行为)。
现在让我们模拟 API。我会保持简单,只模拟我需要测试的内容。重要的是,我希望在一般情况下模拟 API,而不是个别实例的个别方法。后者在某些情况下可能很有用,或者当没有其他办法时,但我需要访问在我的业务逻辑内创建的对象(除非通过某个工厂模式进行注入或提供),而这意味着修改主要源代码。同时,在一个地方模拟 API 也有一个缺点:它是一个通用的解决方案,可能会实现成功执行。对于测试错误情况,可能需要在测试本身的实例中模拟实例,但是在您的业务逻辑中,您可能无法直接访问深处创建的例如post
的实例。
因此,让我们看一下模拟成功 API 调用的一般情况:
test/mock.js
var mongoose = require("mongoose");
mongoose.Collection.prototype.insert = function(docs, options, callback) {
callback(null, docs);
};
module.exports = mongoose;
通常情况下,只要在修改 mongoose 后创建的模型,就可以考虑对上述模拟进行每次测试基础的操作以模拟任何行为。但是在每次测试之前一定要确保恢复到原始状态!
最后,以下是我们针对所有可能的数据保存操作的测试样例。请注意,这些测试并不特定于我们的 Post
模型,可以使用完全相同的模拟方式完成所有其他模型的测试。
test/test_model.js
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);
});
});
});
需要注意的是我们目前仍在测试非常低级别的功能,但我们可以使用同样的方法来测试任何使用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)
我必须说,这样做并不好玩。但是这样做确实是对业务逻辑进行纯单元测试的方式,而没有使用内存或真实数据库,并且相当通用。