De basis
Bij unit testing mag men de DB niet raken. Ik zou één uitzondering kunnen bedenken:het raken van een in-memory DB, maar zelfs dat ligt al op het gebied van integratietesten, omdat je de status die in het geheugen is opgeslagen alleen nodig hebt voor complexe processen (en dus niet echt functionele eenheden). Dus ja, geen werkelijke DB.
Wat u in unit tests wilt testen, is dat uw bedrijfslogica resulteert in correcte API-aanroepen op de interface tussen uw applicatie en de DB. U kunt en moet er waarschijnlijk van uitgaan dat de ontwikkelaars van de DB API/stuurprogramma's goed werk hebben verricht door te testen dat alles onder de API zich gedraagt zoals verwacht. U wilt in uw tests echter ook laten zien hoe uw bedrijfslogica reageert op verschillende geldige API-resultaten, zoals succesvolle opslag, fouten als gevolg van gegevensconsistentie, fouten als gevolg van verbindingsproblemen, enz.
Dit betekent dat wat je nodig hebt en wilt bespotten alles is dat zich onder de DB-stuurprogramma-interface bevindt. U zou dat gedrag echter moeten modelleren zodat uw bedrijfslogica kan worden getest op alle resultaten van de DB-aanroepen.
Makkelijker gezegd dan gedaan, want dit betekent dat je toegang moet hebben tot de API via de technologie die je gebruikt en dat je de API moet kennen.
De realiteit van mangoest
Vasthoudend aan de basis willen we de spot drijven met de oproepen die worden uitgevoerd door de onderliggende 'driver' die mangoest gebruikt. Ervan uitgaande dat het node-mongodb-native
is we moeten de spot drijven met die oproepen. Het volledige samenspel tussen mangoest en de native driver begrijpen is niet eenvoudig, maar het komt over het algemeen neer op de methoden in mongoose.Collection
omdat de laatste mongoldb.Collection
uitbreidt en niet methoden opnieuw implementeren zoals insert
. Als we het gedrag van insert
. kunnen controleren in dit specifieke geval weten we dat we de DB-toegang op API-niveau hebben bespot. Je kunt het traceren in de bron van beide projecten, dat Collection.insert
is echt de native drivermethode.
Voor jouw specifieke voorbeeld heb ik een openbare Git-repository gemaakt met een compleet pakket, maar ik zal alle elementen hier in het antwoord plaatsen.
De oplossing
Persoonlijk vind ik de "aanbevolen" manier van werken met mangoest vrij onbruikbaar:modellen worden meestal gemaakt in de modules waar de bijbehorende schema's zijn gedefinieerd, maar ze hebben al een verbinding nodig. Om meerdere verbindingen te hebben om met totaal verschillende mongodb-databases in hetzelfde project te praten en voor testdoeleinden maakt dit het leven erg moeilijk. In feite, zodra de zorgen volledig zijn gescheiden, wordt mangoest, althans voor mij, bijna onbruikbaar.
Dus het eerste dat ik maak, is het pakketbeschrijvingsbestand, een module met een schema en een generieke "modelgenerator":
{
"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
};
Zo'n modelgenerator heeft zijn nadelen:er zijn mogelijk elementen die aan het model moeten worden gekoppeld en het zou logisch zijn om ze in dezelfde module te plaatsen waar het schema is gemaakt. Dus het vinden van een generieke manier om die toe te voegen is een beetje lastig. Een module kan bijvoorbeeld post-acties exporteren om automatisch te worden uitgevoerd wanneer een model wordt gegenereerd voor een bepaalde verbinding enz. (hacken).
Laten we nu de API bespotten. Ik zal het simpel houden en zal alleen spotten met wat ik nodig heb voor de tests in kwestie. Het is essentieel dat ik de API in het algemeen zou willen bespotten, niet individuele methoden van individuele instanties. Dit laatste kan in sommige gevallen nuttig zijn, of wanneer niets anders helpt, maar ik zou toegang moeten hebben tot objecten die binnen mijn bedrijfslogica zijn gemaakt (tenzij geïnjecteerd of geleverd via een of ander fabriekspatroon), en dit zou betekenen dat de hoofdbron moet worden gewijzigd. Tegelijkertijd heeft het bespotten van de API op één plek een nadeel:het is een generieke oplossing, die waarschijnlijk een succesvolle uitvoering zou implementeren. Voor het testen van foutgevallen kan het nodig zijn om te spotten in instanties in de tests zelf, maar dan heeft u binnen uw bedrijfslogica mogelijk geen directe toegang tot de instantie van b.v. post
diep van binnen gecreëerd.
Laten we dus eens kijken naar het algemene geval van het bespotten van succesvolle API-aanroepen:
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;
Over het algemeen, zolang modellen worden gemaakt na het aanpassen van mangoest, is het denkbaar dat de bovenstaande mocks per test worden gedaan om elk gedrag te simuleren. Zorg er echter voor dat u vóór elke test terugkeert naar het oorspronkelijke gedrag!
Ten slotte is dit hoe onze tests voor alle mogelijke gegevensbesparende bewerkingen eruit zouden kunnen zien. Let op, deze zijn niet specifiek voor onze Post
model en zou voor alle andere modellen kunnen worden gedaan met precies dezelfde mock op zijn plaats.
// 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);
});
});
});
Het is essentieel op te merken dat we nog steeds de functionaliteit op zeer laag niveau testen, maar we kunnen dezelfde aanpak gebruiken voor het testen van elke bedrijfslogica die gebruikmaakt van Post.create
of post.save
intern.
Het allerlaatste, laten we de tests uitvoeren:
> [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)
Ik moet zeggen dat het niet leuk is om het zo te doen. Maar op deze manier is het echt pure unit-testing van de bedrijfslogica zonder enige in-memory of echte DB's en redelijk generiek.