sql >> Database teknologi >  >> NoSQL >> MongoDB

Mocking/stubbing Mongoose model spare metode

Det grundlæggende

Ved enhedstest skal man ikke ramme DB. Jeg kunne tænke på en undtagelse:at ramme en in-memory DB, men selv det ligger allerede inden for integrationstestområdet, da du kun ville have brug for den tilstand, der er gemt i hukommelsen til komplekse processer (og dermed ikke rigtig enheder af funktionalitet). Så ja nej egentlig DB.

Det, du vil teste i enhedstests, er, at din forretningslogik resulterer i korrekte API-kald ved grænsefladen mellem din applikation og DB. Du kan og skal nok antage, at DB API/driverudviklerne har gjort et godt stykke arbejde med at teste, at alt under API'en opfører sig som forventet. Du ønsker dog også at dække i dine tests, hvordan din forretningslogik reagerer på forskellige gyldige API-resultater såsom vellykkede lagringer, fejl på grund af datakonsistens, fejl på grund af forbindelsesproblemer osv.

Det betyder, at det, du har brug for og ønsker at håne, er alt, hvad der er under DB-drivergrænsefladen. Du skal dog modellere denne adfærd, så din forretningslogik kan testes for alle resultater af DB-opkaldene.

Lettere sagt end gjort, fordi det betyder, at du skal have adgang til API'et via den teknologi, du bruger, og du skal kende API'et.

Mangusts virkelighed

Ved at holde os til det grundlæggende, ønsker vi at håne de opkald, der udføres af den underliggende 'driver', som mongoose bruger. Forudsat at det er node-mongodb-native vi er nødt til at håne de opkald. Det er ikke let at forstå det fulde samspil mellem mongoose og den indfødte driver, men det handler generelt om metoderne i mongoose.Collection fordi sidstnævnte udvider mongoldb.Collection og ikke genimplementere metoder som insert . Hvis vi er i stand til at kontrollere adfærden for insert i dette særlige tilfælde ved vi, at vi hånede DB-adgangen på API-niveau. Du kan spore det i kilden til begge projekter, som Collection.insert er virkelig den native driver-metode.

Til dit særlige eksempel oprettede jeg et offentligt Git-lager med en komplet pakke, men jeg vil poste alle elementerne her i svaret.

Løsningen

Personligt finder jeg den "anbefalede" måde at arbejde med mongoose på ret ubrugelig:modeller oprettes normalt i de moduler, hvor de tilsvarende skemaer er defineret, men de har allerede brug for en forbindelse. Med det formål at have flere forbindelser til at tale med helt forskellige mongodb-databaser i det samme projekt og til testformål gør dette livet virkelig svært. Faktisk, så snart bekymringerne er helt adskilt, bliver mangust, i det mindste for mig, næsten ubrugelig.

Så det første jeg opretter er pakkebeskrivelsesfilen, et modul med et skema og en generisk "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
};

En sådan modelgenerator har sine ulemper:Der er elementer, der muligvis skal knyttes til modellen, og det ville give mening at placere dem i det samme modul, hvor skemaet oprettes. Så det er lidt vanskeligt at finde en generisk måde at tilføje dem på. Et modul kunne f.eks. eksportere post-actions til automatisk at blive kørt, når en model genereres for en given forbindelse osv. (hacking).

Lad os nu håne API'en. Jeg vil holde det enkelt og vil kun håne det, jeg har brug for til de pågældende tests. Det er vigtigt, at jeg gerne vil håne API'en generelt, ikke individuelle metoder for individuelle instanser. Sidstnævnte kan være nyttigt i nogle tilfælde, eller når intet andet hjælper, men jeg skal have adgang til objekter, der er oprettet inde i min forretningslogik (medmindre de er injiceret eller leveret via et eller andet fabriksmønster), og det ville betyde, at jeg skulle ændre hovedkilden. Samtidig har det en ulempe at håne API'en ét sted:det er en generisk løsning, som sandsynligvis ville implementere vellykket eksekvering. Til test af fejltilfælde kan det være nødvendigt med hån i instanser i selve testene, men så har du inden for din forretningslogik måske ikke direkte adgang til instansen af ​​f.eks. post skabt dybt inde.

Så lad os se på det generelle tilfælde af at håne et vellykket API-kald:

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;

Generelt, så længe modeller er oprettet efter ved at modificere mongoose, er det tænkeligt, at ovenstående hån udføres på pr. testbasis for at simulere enhver adfærd. Sørg dog for at vende tilbage til den oprindelige adfærd før hver test!

Endelig er det sådan, vores test for alle mulige datalagringsoperationer kunne se ud. Vær opmærksom, disse er ikke specifikke for vores Post model og kunne gøres for alle andre modeller med nøjagtig den samme mock på plads.

// 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 er vigtigt at bemærke, at vi stadig tester funktionaliteten på meget lavt niveau, men vi kan bruge den samme tilgang til at teste enhver forretningslogik, der bruger Post.create eller post.save internt.

Det allersidste, lad os køre testene:

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

Jeg må sige, det er ikke sjovt at gøre det på den måde. Men på denne måde er det virkelig ren enhedstest af forretningslogikken uden nogen in-memory eller rigtige DB'er og ret generisk.



  1. Mongo-forespørgsel på flere felter af underdokument

  2. Opdaterer meteor mongodb-versionen automatisk?

  3. MongoDB - hvordan forespørger man efter et indlejret element i en samling?

  4. Hvis andet hvis kond i mongodb aggregering