Die Grundlagen
Beim Unit-Test sollte man nicht auf die DB treffen. Eine Ausnahme könnte ich mir vorstellen:das Auftreffen auf eine In-Memory-DB, aber auch das liegt schon im Bereich des Integrationstestens, da man für komplexe Prozesse nur den im Speicher abgelegten Zustand (und damit nicht wirklich Funktionseinheiten) benötigt. Also, ja, keine tatsächliche DB.
Was Sie in Komponententests testen möchten, ist, dass Ihre Geschäftslogik zu korrekten API-Aufrufen an der Schnittstelle zwischen Ihrer Anwendung und der DB führt. Sie können und sollten wahrscheinlich davon ausgehen, dass die DB-API-/Treiberentwickler gute Arbeit geleistet haben, um zu testen, dass sich alles unterhalb der API wie erwartet verhält. Sie möchten jedoch in Ihren Tests auch abdecken, wie Ihre Geschäftslogik auf verschiedene gültige API-Ergebnisse reagiert, z. B. erfolgreiche Speicherungen, Fehler aufgrund von Datenkonsistenz, Fehler aufgrund von Verbindungsproblemen usw.
Das bedeutet, dass Sie alles verspotten müssen und wollen, was sich unterhalb der DB-Treiberschnittstelle befindet. Sie müssten dieses Verhalten jedoch modellieren, damit Ihre Geschäftslogik für alle Ergebnisse der DB-Aufrufe getestet werden kann.
Leichter gesagt als getan, denn das bedeutet, dass Sie über die von Ihnen verwendete Technologie Zugriff auf die API haben und die API kennen müssen.
Die Realität des Mungos
Um uns an die Grundlagen zu halten, wollen wir die Anrufe verspotten, die von dem zugrunde liegenden „Treiber“ ausgeführt werden, den Mungo verwendet. Angenommen, es ist node-mongodb-native
wir müssen diese Anrufe verspotten. Das vollständige Zusammenspiel zwischen Mongoose und dem nativen Treiber zu verstehen ist nicht einfach, aber im Allgemeinen kommt es auf die Methoden in mongoose.Collection
an weil letzteres mongoldb.Collection
erweitert und nicht Methoden wie insert
neu implementieren . Wenn wir das Verhalten von insert
steuern können In diesem speziellen Fall wissen wir, dass wir den DB-Zugriff auf API-Ebene verspottet haben. Sie können es in der Quelle beider Projekte nachverfolgen, der Collection.insert
ist wirklich die native Treibermethode.
Für Ihr spezielles Beispiel habe ich ein öffentliches Git-Repository erstellt mit einem kompletten Paket, aber ich werde alle Elemente hier in der Antwort posten.
Die Lösung
Ich persönlich finde die "empfohlene" Arbeitsweise mit Mongoose ziemlich unbrauchbar:Modelle werden normalerweise in den Modulen erstellt, in denen die entsprechenden Schemas definiert sind, aber sie benötigen bereits eine Verbindung. Um mehrere Verbindungen zu haben, um mit völlig unterschiedlichen Mongodb-Datenbanken im selben Projekt zu kommunizieren, und zu Testzwecken macht dies das Leben wirklich schwer. Tatsächlich wird Mungo, sobald die Bedenken vollständig getrennt sind, zumindest für mich nahezu unbrauchbar.
Das erste, was ich erstelle, ist also die Paketbeschreibungsdatei, ein Modul mit einem Schema und einem generischen "Modellgenerator":
{
"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
};
Ein solcher Modellgenerator hat seine Nachteile:Es gibt Elemente, die möglicherweise an das Modell angehängt werden müssen, und es wäre sinnvoll, sie in demselben Modul zu platzieren, in dem das Schema erstellt wird. Es ist also etwas schwierig, einen generischen Weg zu finden, diese hinzuzufügen. Beispielsweise könnte ein Modul Nachaktionen exportieren, die automatisch ausgeführt werden, wenn ein Modell für eine bestimmte Verbindung usw. generiert wird (Hacking).
Lassen Sie uns nun die API verspotten. Ich werde es einfach halten und nur das verspotten, was ich für die betreffenden Tests benötige. Wesentlich ist, dass ich die API im Allgemeinen verspotten möchte, nicht einzelne Methoden einzelner Instanzen. Letzteres könnte in einigen Fällen nützlich sein oder wenn nichts anderes hilft, aber ich müsste Zugriff auf Objekte haben, die innerhalb meiner Geschäftslogik erstellt wurden (es sei denn, sie werden injiziert oder über ein Fabrikmuster bereitgestellt), und dies würde bedeuten, die Hauptquelle zu ändern. Gleichzeitig hat das Spotten der API an einer Stelle einen Nachteil:Es handelt sich um eine generische Lösung, die wahrscheinlich eine erfolgreiche Ausführung implementieren würde. Zum Testen von Fehlerfällen kann es erforderlich sein, Instanzen in den Tests selbst zu verspotten, aber dann haben Sie innerhalb Ihrer Geschäftslogik möglicherweise keinen direkten Zugriff auf die Instanz von z. post
tief im Inneren erschaffen.
Schauen wir uns also den allgemeinen Fall des Mocking eines erfolgreichen API-Aufrufs an:
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;
Im Allgemeinen, solange Modelle nach erstellt werden Mongoose zu modifizieren, ist es denkbar, dass die obigen Mocks auf Testbasis durchgeführt werden, um jedes Verhalten zu simulieren. Stellen Sie jedoch sicher, dass Sie vor jedem Test zum ursprünglichen Verhalten zurückkehren!
So könnten schließlich unsere Tests für alle möglichen Datenspeicherungsoperationen aussehen. Achtung, diese sind nicht spezifisch für unseren Post
Modell und könnte für alle anderen Modelle mit genau demselben Mock durchgeführt werden.
// 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);
});
});
});
Es ist wichtig zu beachten, dass wir immer noch die Funktionalität auf sehr niedriger Ebene testen, aber wir können denselben Ansatz zum Testen jeder Geschäftslogik verwenden, die Post.create
verwendet oder post.save
intern.
Der allerletzte Teil, lassen Sie uns die Tests durchführen:
> [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)
Ich muss sagen, das macht keinen Spaß, es so zu machen. Aber auf diese Weise ist es wirklich ein reines Unit-Testing der Geschäftslogik ohne In-Memory- oder echte DBs und ziemlich generisch.