sql >> Databasteknik >  >> NoSQL >> MongoDB

Mocking/stubbing Mongoose modell spara metod

Grunderna

Vid enhetstestning bör man inte träffa DB. Jag skulle kunna tänka mig ett undantag:att träffa en in-memory DB, men även det ligger redan inom området för integrationstestning eftersom du bara skulle behöva tillståndet sparat i minnet för komplexa processer (och alltså inte riktigt enheter av funktionalitet). Så, ja, ingen faktisk DB.

Det du vill testa i enhetstester är att din affärslogik resulterar i korrekta API-anrop i gränssnittet mellan din applikation och DB. Du kan och bör antagligen anta att utvecklarna av DB API/drivrutin har gjort ett bra jobb med att testa att allt under API:et beter sig som förväntat. Men du vill också ta upp i dina tester hur din affärslogik reagerar på olika giltiga API-resultat som framgångsrika besparingar, misslyckanden på grund av datakonsistens, misslyckanden på grund av anslutningsproblem etc.

Det betyder att det du behöver och vill håna är allt som finns under DB-drivrutinens gränssnitt. Du skulle dock behöva modellera det beteendet så att din affärslogik kan testas för alla resultat av DB-anropen.

Lättare sagt än gjort eftersom det betyder att du måste ha tillgång till API:t via den teknik du använder och att du behöver känna till API:t.

Mangusts verklighet

Genom att hålla oss till grunderna vill vi håna samtalen som utförs av den underliggande "föraren" som mongoose använder. Förutsatt att det är node-mongodb-native vi måste håna de samtalen. Det är inte lätt att förstå hela samspelet mellan mongoose och den inhemska föraren, men det handlar i allmänhet om metoderna i mongoose.Collection eftersom den senare utökar mongoldb.Collection och inte återimplementera metoder som insert . Om vi ​​kan kontrollera beteendet för insert i det här specifika fallet vet vi att vi hånade DB-åtkomsten på API-nivå. Du kan spåra det i källan för båda projekten, som Collection.insert är verkligen den ursprungliga drivrutinsmetoden.

För ditt specifika exempel skapade jag ett offentligt Git-förråd med ett komplett paket, men jag kommer att lägga upp alla element här i svaret.

Lösningen

Personligen tycker jag att det "rekommenderade" sättet att arbeta med mongoose är ganska oanvändbart:modeller skapas vanligtvis i de moduler där motsvarande scheman är definierade, men de behöver redan en anslutning. I syfte att ha flera anslutningar för att prata med helt olika mongodb-databaser i samma projekt och för teständamål gör detta livet riktigt svårt. Faktum är att så fort bekymmer är helt separerade blir mungo, åtminstone för mig, nästan oanvändbar.

Så det första jag skapar är paketbeskrivningsfilen, en modul med ett schema och en generisk "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
};

En sådan modellgenerator har sina nackdelar:det finns element som kan behöva kopplas till modellen och det skulle vara vettigt att placera dem i samma modul där schemat skapas. Så att hitta ett generiskt sätt att lägga till dessa är lite knepigt. En modul kan till exempel exportera post-actions för att köras automatiskt när en modell genereras för en given anslutning etc. (hacking).

Låt oss nu håna API:et. Jag ska hålla det enkelt och kommer bara att håna det jag behöver för testerna i fråga. Det är viktigt att jag skulle vilja håna API i allmänhet, inte individuella metoder för enskilda instanser. Det senare kan vara användbart i vissa fall, eller när inget annat hjälper, men jag skulle behöva ha tillgång till objekt som skapats inuti min affärslogik (såvida de inte injiceras eller tillhandahålls via något fabriksmönster), och detta skulle innebära att modifiera huvudkällan. Samtidigt har det en nackdel att håna API:et på ett ställe:det är en generisk lösning som förmodligen skulle implementera framgångsrik exekvering. För att testa felfall kan hån i instanser i själva testerna krävas, men då kanske du inom din affärslogik inte har direkt tillgång till instansen av t.ex. post skapat djupt inuti.

Så låt oss ta en titt på det allmänna fallet med att håna framgångsrika API-anrop:

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;

I allmänhet, så länge som modeller skapas efter Om man modifierar mongoose, är det tänkbart att ovanstående hån görs på testbasis för att simulera något beteende. Se dock till att återgå till det ursprungliga beteendet före varje test!

Äntligen är det så här våra tester för alla möjliga datalagringsoperationer skulle kunna se ut. Var uppmärksam, dessa är inte specifika för vårt Post modell och skulle kunna göras för alla andra modeller med exakt samma mock på plats.

// 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);
        });
    });

});

Det är viktigt att notera att vi fortfarande testar funktionaliteten på mycket låg nivå, men vi kan använda samma metod för att testa vilken affärslogik som helst som använder Post.create eller post.save internt.

Den allra sista biten, låt oss köra testerna:

> [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)

Jag måste säga att det inte är kul att göra på det sättet. Men på detta sätt är det verkligen ren enhetstestning av affärslogiken utan några in-memory eller riktiga DB:er och ganska generisk.



  1. automatisk inkrement med loopback.js och MongoDB

  2. Mongoid:hur frågar man efter alla objekt där värdet är noll?

  3. Hur kan jag räkna månadsvis nutid i laravel?

  4. Utveckla databasschema för Notify som facebook