Základy
Při testování jednotek by člověk neměl zasáhnout DB. Napadá mě jedna výjimka:trefit se do in-memory DB, ale i to už leží v oblasti integračního testování, protože stav uložený v paměti byste potřebovali pouze pro složité procesy (a tedy ve skutečnosti ne jednotky funkčnosti). Takže ano, žádné skutečné DB.
V jednotkových testech chcete otestovat, že vaše obchodní logika má za následek správná volání API na rozhraní mezi vaší aplikací a DB. Můžete a pravděpodobně byste měli předpokládat, že vývojáři DB API/ovladače odvedli dobrou práci při testování, že vše pod API se chová podle očekávání. Ve svých testech však také chcete pokrýt, jak vaše obchodní logika reaguje na různé platné výsledky API, jako jsou úspěšné uložení, selhání kvůli konzistenci dat, selhání kvůli problémům s připojením atd.
To znamená, že to, co potřebujete a chcete zesměšňovat, je vše, co je pod rozhraním ovladače DB. Toto chování byste však museli modelovat, aby mohla být vaše obchodní logika testována na všechny výsledky volání DB.
Snáze se to řekne, než udělá, protože to znamená, že musíte mít přístup k rozhraní API prostřednictvím technologie, kterou používáte, a musíte rozhraní API znát.
Realita mangusty
Držíme-li se základů, chceme zesměšňovat hovory prováděné základním „ovladačem“, který používá mangusta. Za předpokladu, že je node-mongodb-native
musíme tyto hovory zesměšnit. Pochopení úplné souhry mezi mongoose a nativním ovladačem není snadné, ale obecně jde o metody v mongoose.Collection
protože druhý rozšiřuje mongoldb.Collection
a není reimplementovat metody jako insert
. Pokud jsme schopni ovládat chování insert
v tomto konkrétním případě pak víme, že jsme zesměšnili přístup k DB na úrovni API. Můžete to vysledovat ve zdroji obou projektů, v tom Collection.insert
je ve skutečnosti metoda nativního ovladače.
Pro váš konkrétní příklad jsem vytvořil veřejné úložiště Git s kompletním balíčkem, ale všechny prvky zveřejním zde v odpovědi.
Řešení
Osobně považuji „doporučený“ způsob práce s mangustou za dost nepoužitelný:modely se většinou vytvářejí v modulech, kde jsou definována příslušná schémata, přesto už potřebují propojení. Pro účely více připojení pro komunikaci se zcela odlišnými databázemi mongodb ve stejném projektu a pro účely testování to dělá život opravdu těžkým. Ve skutečnosti, jakmile jsou obavy plně odděleny, mangusta, alespoň pro mě, se stává téměř nepoužitelnou.
Takže první věc, kterou vytvořím, je soubor popisu balíčku, modul se schématem a obecný „generátor modelu“:
{
"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
};
Takový generátor modelu má své nevýhody:existují prvky, které může být nutné připojit k modelu a mělo by smysl umístit je do stejného modulu, kde se vytváří schéma. Takže najít obecný způsob, jak je přidat, je trochu složité. Modul by například mohl exportovat následné akce, aby se automaticky spouštěly při generování modelu pro dané připojení atd. (hackování).
Nyní se pojďme vysmívat API. Budu to jednoduché a budu se jen vysmívat tomu, co potřebuji pro příslušné testy. Podstatné je, že bych chtěl zesměšňovat API obecně, ne jednotlivé metody jednotlivých instancí. To druhé by mohlo být užitečné v některých případech, nebo když nic jiného nepomáhá, ale potřeboval bych mít přístup k objektům vytvořeným uvnitř mé obchodní logiky (pokud nejsou injektovány nebo poskytovány prostřednictvím nějakého továrního vzoru), což by znamenalo úpravu hlavního zdroje. Výsměch API na jednom místě má zároveň nevýhodu:jde o generické řešení, které by pravděpodobně implementovalo úspěšné provedení. Pro testování případů chyb může být vyžadováno zesměšňování instancí v samotných testech, ale pak v rámci vaší obchodní logiky nemusíte mít přímý přístup k instanci např. post
vytvořený hluboko uvnitř.
Pojďme se tedy podívat na obecný případ zesměšňování úspěšného volání 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;
Obecně platí, že pokud jsou modely vytvořeny po Při modifikaci mongoose je myslitelné, že výše uvedené simulace jsou prováděny na základě testu, aby se simulovalo jakékoli chování. Ujistěte se však, že se před každým testem vrátíte k původnímu chování!
Konečně takto by mohly vypadat naše testy všech možných operací ukládání dat. Věnujte pozornost, tyto nejsou specifické pro náš Post
model a mohl by být proveden pro všechny ostatní modely s přesně stejným modelem na místě.
// 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);
});
});
});
Je nezbytné poznamenat, že stále testujeme funkčnost na velmi nízké úrovni, ale stejný přístup můžeme použít k testování jakékoli obchodní logiky, která používá Post.create
nebo post.save
interně.
Úplně poslední kousek, spusťte 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)
Musím říct, že to není žádná legrace dělat to takhle. Ale tímto způsobem je to opravdu čisté unit-testování obchodní logiky bez jakýchkoliv in-memory nebo skutečných DB a docela obecné.