Ответ 1
Основы
В модульном тестировании не следует ударять по БД. Я мог бы подумать об одном исключении: попадание в БД памяти, но даже это уже находится в области интеграционного тестирования, поскольку вам потребуется только состояние, сохраненное в памяти для сложных процессов (и, следовательно, не действительно единицы функциональности). Итак, да нет фактической БД.
Что вы хотите протестировать в модульных тестах, так это то, что ваша бизнес-логика приводит к правильным вызовам API на интерфейсе между вашим приложением и БД. Вы можете и, вероятно, должны предположить, что разработчики DB API/драйвера провели хорошую проверку работы, что все под API ведет себя так, как ожидалось. Тем не менее, вы также хотите осветить в своих тестах, как ваша бизнес-логика реагирует на различные достоверные результаты API, такие как успешные сбережения, сбои из-за согласованности данных, сбои из-за проблем подключения и т.д.
Это означает, что то, что вам нужно и которое нужно высмеять, - это все, что находится ниже интерфейса драйвера DB. Однако вам нужно будет моделировать это поведение, чтобы ваша бизнес-логика могла быть протестирована для всех результатов вызовов БД.
Легче сказать, чем сделать, потому что это означает, что вам нужно иметь доступ к API через технологию, которую вы используете, и вам нужно знать API.
Реальность мангуста
Придерживаясь основ, мы хотим издеваться над вызовами, выполняемыми базовым "драйвером", который использует mongoose. Предполагая, что это 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": "*"
}
}
ЦСИ/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;
ЦСИ/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 в целом, а не индивидуальными методами отдельных экземпляров. Последнее может быть полезно в некоторых случаях или когда ничего больше не помогает, но мне нужно будет иметь доступ к объектам, созданным внутри моей бизнес-логики (если они не введены или не предоставлены через некоторый шаблон factory), и это будет означать изменение основного источник. В то же время издевательский 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;
Как правило, до тех пор, пока модели создаются после изменения мангуста, можно подумать, что вышеупомянутые макеты выполняются на каждой тестовой основе для имитации любого поведения. Однако перед каждым тестированием обязательно вернитесь к исходному поведению!
Наконец, так выглядят наши тесты для всех возможных операций сохранения данных. Обратите внимание, что это не относится к нашей модели 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
или post.save
.
Самый последний бит, пусть запускают тесты:
~/source/web/xxx $npm test
> [email protected] 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)
Я должен сказать, что это не забавно делать так. Но таким образом это действительно чисто модульное тестирование бизнес-логики без каких-либо встроенных или реальных БД и довольно общий.