Le basi
In unit test non si dovrebbe colpire il DB. Potrei pensare a un'eccezione:colpire un DB in memoria, ma anche questo si trova già nell'area dei test di integrazione poiché avresti bisogno solo dello stato salvato in memoria per processi complessi (e quindi non proprio unità di funzionalità). Quindi, sì, nessun DB effettivo.
Quello che vuoi testare negli unit test è che la tua logica di business produca chiamate API corrette nell'interfaccia tra la tua applicazione e il DB. Puoi e probabilmente dovresti presumere che gli sviluppatori di API/driver DB abbiano svolto un buon lavoro verificando che tutto ciò che si trova al di sotto dell'API si comporti come previsto. Tuttavia, vuoi anche illustrare nei tuoi test come la tua logica aziendale reagisce a diversi risultati API validi come salvataggi riusciti, errori dovuti alla coerenza dei dati, errori dovuti a problemi di connessione ecc.
Ciò significa che ciò di cui hai bisogno e vuoi deridere è tutto ciò che si trova sotto l'interfaccia del driver DB. Tuttavia, dovresti modellare quel comportamento in modo che la tua logica aziendale possa essere testata per tutti i risultati delle chiamate DB.
Più facile a dirsi che a farsi perché questo significa che devi avere accesso all'API tramite la tecnologia che usi e devi conoscere l'API.
La realtà della mangusta
Attenendoci alle basi, vogliamo prendere in giro le chiamate eseguite dal "driver" sottostante utilizzato dalla mangusta. Supponendo che sia node-mongodb-native
dobbiamo prendere in giro quelle chiamate. Comprendere l'interazione completa tra mongoose e il driver nativo non è facile, ma generalmente dipende dai metodi in mongoose.Collection
perché quest'ultimo estende mongoldb.Collection
e non reimplementare metodi come insert
. Se siamo in grado di controllare il comportamento di insert
in questo caso particolare, allora sappiamo di aver deriso l'accesso al DB a livello di API. Puoi rintracciarlo nel sorgente di entrambi i progetti, quel Collection.insert
è davvero il metodo del driver nativo.
Per il tuo esempio particolare ho creato un repository Git pubblico con un pacchetto completo, ma pubblicherò tutti gli elementi qui nella risposta.
La soluzione
Personalmente trovo il modo "consigliato" di lavorare con mangusta abbastanza inutilizzabile:i modelli vengono solitamente creati nei moduli in cui sono definiti gli schemi corrispondenti, ma necessitano già di una connessione. Allo scopo di avere più connessioni per parlare con database mongodb completamente diversi nello stesso progetto e per scopi di test, questo rende la vita davvero difficile. In effetti, non appena le preoccupazioni sono completamente separate la mangusta, almeno per me, diventa quasi inutilizzabile.
Quindi la prima cosa che creo è il file di descrizione del pacchetto, un modulo con uno schema e un generico "generatore di modelli":
{
"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
};
Un tale generatore di modelli ha i suoi svantaggi:ci sono elementi che potrebbero dover essere allegati al modello e avrebbe senso inserirli nello stesso modulo in cui viene creato lo schema. Quindi trovare un modo generico per aggiungerli è un po' complicato. Ad esempio, un modulo potrebbe esportare post-azioni da eseguire automaticamente quando viene generato un modello per una determinata connessione ecc. (hacking).
Ora prendiamo in giro l'API. Lo terrò semplice e prenderò in giro solo ciò di cui ho bisogno per i test in questione. È essenziale che io voglia prendere in giro l'API in generale, non i singoli metodi delle singole istanze. Quest'ultimo potrebbe essere utile in alcuni casi, o quando nient'altro aiuta, ma avrei bisogno di avere accesso agli oggetti creati all'interno della mia logica di business (a meno che non siano iniettati o forniti tramite un pattern di fabbrica), e questo significherebbe modificare la fonte principale. Allo stesso tempo, deridere l'API in un punto ha uno svantaggio:è una soluzione generica, che probabilmente implementerebbe un'esecuzione riuscita. Per testare i casi di errore, potrebbe essere necessario prendere in giro le istanze nei test stessi, ma all'interno della tua logica aziendale potresti non avere accesso diretto all'istanza ad es. post
creato nel profondo.
Quindi, diamo un'occhiata al caso generale di deridere una chiamata API riuscita:
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;
In genere, purché i modelli vengano creati dopo modificando la mangusta, è pensabile che le simulazioni di cui sopra siano eseguite per test per simulare qualsiasi comportamento. Assicurati di ripristinare il comportamento originale, tuttavia, prima di ogni test!
Finalmente ecco come potrebbero apparire i nostri test per tutte le possibili operazioni di salvataggio dei dati. Fai attenzione, questi non sono specifici per il nostro Post
modello e potrebbe essere fatto per tutti gli altri modelli con esattamente lo stesso mock in atto.
// 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);
});
});
});
È essenziale notare che stiamo ancora testando la funzionalità di livello molto basso, ma possiamo utilizzare questo stesso approccio per testare qualsiasi logica aziendale che utilizzi Post.create
o post.save
internamente.
L'ultimo bit, eseguiamo i 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)
Devo dire che non è divertente farlo in questo modo. Ma in questo modo è davvero un puro unit test della logica aziendale senza alcun DB in memoria o reale e abbastanza generico.