Podstawy
W testach jednostkowych nie należy uderzać w DB. Mógłbym wymyślić jeden wyjątek:uderzenie w DB in-memory, ale nawet to leży już w obszarze testowania integracji, ponieważ dla złożonych procesów (a więc nie do końca jednostki funkcjonalności) potrzebny byłby tylko stan zapisany w pamięci. Więc tak, nie ma rzeczywistej bazy danych.
To, co chcesz przetestować w testach jednostkowych, polega na tym, że logika biznesowa skutkuje poprawnymi wywołaniami interfejsu API na interfejsie między aplikacją a bazą danych. Możesz i prawdopodobnie powinieneś założyć, że programiści DB API/sterowniki wykonali dobre testy pracy, że wszystko poniżej API zachowuje się zgodnie z oczekiwaniami. Jednak w swoich testach chcesz również uwzględnić, w jaki sposób logika biznesowa reaguje na różne prawidłowe wyniki API, takie jak pomyślne zapisywanie, awarie spowodowane spójnością danych, awarie spowodowane problemami z połączeniem itp.
Oznacza to, że to, czego potrzebujesz i chcesz zakpić, to wszystko, co znajduje się poniżej interfejsu sterownika DB. Konieczne byłoby jednak modelowanie tego zachowania, aby można było przetestować logikę biznesową pod kątem wszystkich wyników wywołań bazy danych.
Łatwiej powiedzieć niż zrobić, ponieważ oznacza to, że musisz mieć dostęp do interfejsu API za pośrednictwem używanej technologii i musisz znać interfejs API.
Rzeczywistość mangusty
Trzymając się podstaw, chcemy kpić z połączeń wykonywanych przez podstawowego „sterownika”, którego używa mangusta. Zakładając, że jest to node-mongodb-native
musimy wykpić te telefony. Zrozumienie pełnego współdziałania między mangustą a natywnym sterownikiem nie jest łatwe, ale generalnie sprowadza się do metod w mongoose.Collection
ponieważ ten ostatni rozszerza mongoldb.Collection
a nie ponowne zaimplementowanie metod, takich jak insert
. Jeśli jesteśmy w stanie kontrolować zachowanie insert
w tym konkretnym przypadku wiemy, że wykpiliśmy dostęp do bazy danych na poziomie API. Możesz to prześledzić w źródle obu projektów, że Collection.insert
jest tak naprawdę natywną metodą sterownika.
Dla twojego konkretnego przykładu utworzyłem publiczne repozytorium Git z kompletnym pakietem, ale wszystkie elementy opublikuję tutaj w odpowiedzi.
Rozwiązanie
Osobiście uważam, że "zalecany" sposób pracy z mangustą jest zupełnie bezużyteczny:modele są zwykle tworzone w modułach, w których zdefiniowane są odpowiednie schematy, ale wymagają już połączenia. W celu posiadania wielu połączeń, aby komunikować się z zupełnie różnymi bazami danych mongodb w tym samym projekcie i do celów testowych, to bardzo utrudnia życie. W rzeczywistości, jak tylko obawy zostaną całkowicie oddzielone, mangusta, przynajmniej dla mnie, staje się prawie bezużyteczna.
Pierwszą rzeczą, którą tworzę, jest plik opisu pakietu, moduł ze schematem i ogólny "generator modeli":
{
"name": "xxx",
"version": "0.1.0",
"private": true,
"main": "./src",
"scripts": {
"test" : "mocha --recursive"
},
"dependencies": {
"mongoose": "*"
},
"devDependencies": {
"mocha": "*",
"chai": "*"
}
}
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
};
Taki generator modeli ma swoje wady:są elementy, które mogą wymagać dołączenia do modelu i sensowne byłoby umieszczenie ich w tym samym module, w którym tworzony jest schemat. Więc znalezienie ogólnego sposobu na ich dodanie jest trochę trudne. Na przykład, moduł może eksportować post-akcje, aby były automatycznie uruchamiane, gdy model jest generowany dla danego połączenia itp. (hacking).
Teraz wyśmiejmy API. Poproszę o prostotę i będę kpił tylko z tego, czego potrzebuję do testów, o których mowa. Niezbędne jest to, że chciałbym wyśmiewać API w ogóle, a nie poszczególne metody poszczególnych instancji. Ta ostatnia może być przydatna w niektórych przypadkach lub gdy nic innego nie pomaga, ale musiałbym mieć dostęp do obiektów utworzonych w mojej logice biznesowej (chyba że wstrzyknięto je lub zapewniono przez jakiś wzorzec fabryczny), a to oznaczałoby modyfikację głównego źródła. Jednocześnie mockowanie API w jednym miejscu ma wadę:jest to rozwiązanie generyczne, które prawdopodobnie zaimplementuje pomyślne wykonanie. W przypadku testowania przypadków błędów może być wymagane mockowanie w instancjach w samych testach, ale wtedy w ramach logiki biznesowej możesz nie mieć bezpośredniego dostępu do instancji np. post
stworzony głęboko w środku.
Przyjrzyjmy się więc ogólnemu przypadkowi naśladowania udanego wywołania API:
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;
Generalnie, o ile modele są tworzone po modyfikując mangusty, można pomyśleć, że powyższe makiety są wykonywane na podstawie testu, aby zasymulować dowolne zachowanie. Pamiętaj jednak, aby powrócić do pierwotnego zachowania przed każdym testem!
W końcu tak mogą wyglądać nasze testy dla wszystkich możliwych operacji oszczędzania danych. Zwróć uwagę, nie są one specyficzne dla naszego Post
model i można to zrobić dla wszystkich innych modeli z dokładnie taką samą makią.
// 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);
});
});
});
Należy zauważyć, że nadal testujemy funkcjonalność bardzo niskiego poziomu, ale możemy użyć tego samego podejścia do testowania dowolnej logiki biznesowej, która używa Post.create
lub post.save
wewnętrznie.
Ostatnia część, przeprowadźmy testy:
> [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)
Muszę powiedzieć, że to nie jest zabawne robić to w ten sposób. Ale w ten sposób jest to naprawdę czysty test jednostkowy logiki biznesowej bez żadnych baz danych w pamięci lub rzeczywistych baz danych i dość ogólny.