基础
在单元测试中,不应访问数据库。我可以想到一个例外:访问内存中的数据库,但即使这已经属于集成测试的范围,因为您只需要为复杂的进程保存在内存中的状态(因此不是真正的功能单元)。所以,是的,没有实际的数据库。
您想要在单元测试中测试的是您的业务逻辑在应用程序和数据库之间的接口处产生正确的 API 调用。您可以而且可能应该假设数据库 API/驱动程序开发人员已经完成了良好的测试,确保 API 下面的所有内容都按预期运行。但是,您还希望在测试中涵盖业务逻辑如何对不同的有效 API 结果做出反应,例如成功保存、由于数据一致性而失败、由于连接问题而失败等。
这意味着您需要并想要模拟的是数据库驱动程序接口下方的所有内容。但是,您需要对该行为进行建模,以便可以针对数据库调用的所有结果测试您的业务逻辑。
说起来容易做起来难,因为这意味着您需要通过您使用的技术访问 API,并且您需要了解 API。
猫鼬的现实
坚持基础知识,我们想要模拟 mongoose 使用的底层“驱动程序”执行的调用。假设是节点 mongodb-native https://github.com/mongodb/node-mongodb-native我们需要模拟这些调用。理解 mongoose 和本机驱动程序之间的完整相互作用并不容易,但它通常归结为以下方法:mongoose.Collection
因为后者延伸了mongoldb.Collection
and does not重新实现方法,例如insert
。如果我们能够控制人的行为insert
在这种特殊情况下,我们知道我们在 API 级别模拟了数据库访问。您可以在两个项目的源代码中追踪它,即Collection.insert
确实是本机驱动程序方法。
对于我创建的特定示例公共 Git 存储库 https://code.teris.io/parked/mongoose-mock-example具有完整的包,但我将在答案中发布所有元素。
解决方案
就我个人而言,我发现使用 mongoose 的“推荐”方式非常不可用:模型通常在定义相应模式的模块中创建,但它们已经需要连接。为了在同一个项目中拥有多个连接来与完全不同的 mongodb 数据库进行通信以及为了测试目的,这让生活变得非常困难。事实上,一旦关注点完全分离,猫鼬,至少对我来说,就变得几乎无法使用。
所以我创建的第一件事是包描述文件,一个带有模式的模块和一个通用的“模型生成器”:
包.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
};
这样的模型生成器有其缺点:可能需要将某些元素附加到模型,并且将它们放置在创建模式的同一模块中是有意义的。因此,找到一种通用的方法来添加这些内容有点棘手。例如,模块可以导出后操作,以便在为给定连接生成模型时自动运行等(黑客攻击)。
现在让我们模拟 API。我会保持简单,只会模拟相关测试所需的内容。重要的是,我想模拟一般的 API,而不是单个实例的单个方法。后者在某些情况下可能很有用,或者当没有其他帮助时,但我需要访问在业务逻辑内部创建的对象(除非通过某种工厂模式注入或提供),这意味着修改主要源代码。同时,在一个地方模拟 API 有一个缺点:它是一种通用解决方案,可能会实现成功执行。对于测试错误情况,可能需要在测试本身的实例中进行模拟,但是在您的业务逻辑中,您可能无法直接访问例如post
内心深处创造的。
那么,让我们看一下模拟成功 API 调用的一般情况:
测试/模拟.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;
一般只要创建好模型after修改猫鼬,可以想象上述模拟是在每个测试的基础上完成的,以模拟任何行为。但是,在每次测试之前,请确保恢复到原始行为!
最后,这就是我们对所有可能的数据保存操作的测试的样子。注意,这些并不是我们特有的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);
});
});
});
值得注意的是,我们仍在测试非常低级别的功能,但我们可以使用相同的方法来测试使用的任何业务逻辑Post.create
or post.save
内部。
最后一点,让我们运行测试:
〜/source/web/xxx $ npm 测试
> [email protected] /cdn-cgi/l/email-protection 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)
我必须说,这样做一点也不有趣。但通过这种方式,它实际上是业务逻辑的纯粹单元测试,没有任何内存中或真实的数据库,并且相当通用。