MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Méthode de sauvegarde du modèle Mocking/stubbing Mongoose

Les bases

Dans les tests unitaires, il ne faut pas toucher la base de données. Je pourrais penser à une exception:frapper une base de données en mémoire, mais même cela se situe déjà dans le domaine des tests d'intégration car vous n'auriez besoin que de l'état enregistré en mémoire pour les processus complexes (et donc pas vraiment d'unités de fonctionnalité). Donc, oui, pas de base de données réelle.

Ce que vous voulez tester dans les tests unitaires, c'est que votre logique métier aboutit à des appels d'API corrects à l'interface entre votre application et la base de données. Vous pouvez et devriez probablement supposer que les développeurs de l'API/du pilote DB ont fait un bon travail en testant que tout ce qui se trouve en dessous de l'API se comporte comme prévu. Cependant, vous souhaitez également couvrir dans vos tests la manière dont votre logique métier réagit à différents résultats d'API valides, tels que des sauvegardes réussies, des échecs dus à la cohérence des données, des échecs dus à des problèmes de connexion, etc.

Cela signifie que ce dont vous avez besoin et que vous voulez simuler, c'est tout ce qui se trouve sous l'interface du pilote de base de données. Vous devrez cependant modéliser ce comportement afin que votre logique métier puisse être testée pour tous les résultats des appels de base de données.

Plus facile à dire qu'à faire, car cela signifie que vous devez avoir accès à l'API via la technologie que vous utilisez et que vous devez connaître l'API.

La réalité de la mangouste

En nous en tenant aux bases, nous voulons nous moquer des appels effectués par le "pilote" sous-jacent utilisé par la mangouste. En supposant qu'il s'agisse de node-mongodb-native nous devons nous moquer de ces appels. Comprendre l'interaction complète entre la mangouste et le pilote natif n'est pas facile, mais cela se résume généralement aux méthodes de mongoose.Collection car ce dernier étend mongoldb.Collection et ne le fait pas réimplémenter des méthodes comme insert . Si nous sommes capables de contrôler le comportement de insert dans ce cas particulier, nous savons que nous nous sommes moqués de l'accès à la base de données au niveau de l'API. Vous pouvez le tracer dans la source des deux projets, que Collection.insert est vraiment la méthode du pilote natif.

Pour votre exemple particulier, j'ai créé un référentiel Git public avec un package complet, mais je posterai tous les éléments ici dans la réponse.

La solution

Personnellement, je trouve la manière "recommandée" de travailler avec mangouste assez inutilisable :les modèles sont généralement créés dans les modules où les schémas correspondants sont définis, mais ils ont déjà besoin d'une connexion. Dans le but d'avoir plusieurs connexions pour parler à des bases de données mongodb complètement différentes dans le même projet et à des fins de test, cela rend la vie très difficile. En fait, dès que les préoccupations sont complètement séparées, la mangouste, du moins pour moi, devient presque inutilisable.

Donc, la première chose que je crée est le fichier de description du package, un module avec un schéma et un "générateur de modèle" générique :

{
  "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 tel générateur de modèle a ses inconvénients :il y a des éléments qui peuvent devoir être attachés au modèle et il serait logique de les placer dans le même module où le schéma est créé. Donc, trouver un moyen générique d'ajouter ceux-ci est un peu délicat. Par exemple, un module pourrait exporter des post-actions à exécuter automatiquement lorsqu'un modèle est généré pour une connexion donnée, etc. (piratage).

Maintenant, moquons-nous de l'API. Je vais rester simple et ne me moquerai que de ce dont j'ai besoin pour les tests en question. Il est essentiel que je souhaite simuler l'API en général, et non les méthodes individuelles d'instances individuelles. Ce dernier peut être utile dans certains cas, ou lorsque rien d'autre n'aide, mais j'aurais besoin d'avoir accès aux objets créés à l'intérieur de ma logique métier (sauf s'ils sont injectés ou fournis via un modèle d'usine), ce qui signifierait modifier la source principale. Dans le même temps, se moquer de l'API à un endroit présente un inconvénient :il s'agit d'une solution générique, qui implémenterait probablement une exécution réussie. Pour tester les cas d'erreur, il peut être nécessaire de se moquer des instances dans les tests eux-mêmes, mais dans votre logique métier, vous n'aurez peut-être pas un accès direct à l'instance, par exemple. post créé profondément à l'intérieur.

Examinons donc le cas général d'un appel d'API réussi :

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;

Généralement, tant que les modèles sont créés après modifiant la mangouste, il est concevable que les simulations ci-dessus soient effectuées par test pour simuler n'importe quel comportement. Assurez-vous cependant de revenir au comportement d'origine avant chaque test !

Enfin, voici à quoi pourraient ressembler nos tests pour toutes les opérations de sauvegarde de données possibles. Attention, ceux-ci ne sont pas spécifiques à notre Post modèle et pourrait être fait pour tous les autres modèles avec exactement la même maquette en place.

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

});

Il est essentiel de noter que nous testons toujours la fonctionnalité de très bas niveau, mais nous pouvons utiliser cette même approche pour tester toute logique métier qui utilise Post.create ou post.save en interne.

La toute dernière partie, lançons les tests :

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

Je dois dire que ce n'est pas amusant de le faire de cette façon. Mais de cette façon, il s'agit vraiment de tests unitaires purs de la logique métier sans aucune base de données en mémoire ou réelle et assez générique.