sql >> Base de Datos >  >> NoSQL >> MongoDB

Método de guardado del modelo Mongoose burlándose/aplastando

Lo básico

En las pruebas unitarias, uno no debe golpear el DB. Podría pensar en una excepción:acceder a una base de datos en memoria, pero incluso eso ya se encuentra en el área de pruebas de integración, ya que solo necesitaría el estado guardado en la memoria para procesos complejos (y, por lo tanto, no realmente unidades de funcionalidad). Entonces, sí, no DB real.

Lo que desea probar en las pruebas unitarias es que su lógica comercial dé como resultado llamadas API correctas en la interfaz entre su aplicación y la base de datos. Puede y probablemente debería suponer que los desarrolladores de la API/controlador de la base de datos han hecho un buen trabajo probando que todo lo que se encuentra debajo de la API se comporta como se esperaba. Sin embargo, también desea cubrir en sus pruebas cómo reacciona su lógica comercial a diferentes resultados de API válidos, como guardados exitosos, fallas debido a la consistencia de los datos, fallas debido a problemas de conexión, etc.

Esto significa que lo que necesita y quiere simular es todo lo que está debajo de la interfaz del controlador DB. Sin embargo, necesitaría modelar ese comportamiento para que su lógica comercial pueda probarse para todos los resultados de las llamadas a la base de datos.

Es más fácil decirlo que hacerlo porque esto significa que necesita tener acceso a la API a través de la tecnología que usa y necesita conocer la API.

La realidad de la mangosta

Siguiendo con lo básico, queremos simular las llamadas realizadas por el 'controlador' subyacente que usa Mongoose. Asumiendo que es node-mongodb-native tenemos que burlarnos de esas llamadas. Comprender la interacción completa entre mongoose y el controlador nativo no es fácil, pero generalmente se reduce a los métodos en mongoose.Collection porque este último extiende mongoldb.Collection y no reimplementar métodos como insert . Si somos capaces de controlar el comportamiento de insert en este caso particular, sabemos que burlamos el acceso a la base de datos en el nivel de la API. Puede rastrearlo en la fuente de ambos proyectos, que Collection.insert es realmente el método de controlador nativo.

Para su ejemplo particular, creé un repositorio Git público con un paquete completo, pero publicaré todos los elementos aquí en la respuesta.

La solución

Personalmente, la forma "recomendada" de trabajar con mongoose me parece bastante inutilizable:los modelos generalmente se crean en los módulos donde se definen los esquemas correspondientes, pero ya necesitan una conexión. Con el fin de tener múltiples conexiones para hablar con bases de datos mongodb completamente diferentes en el mismo proyecto y con fines de prueba, esto hace que la vida sea realmente difícil. De hecho, tan pronto como las preocupaciones se separan por completo, la mangosta, al menos para mí, se vuelve casi inutilizable.

Entonces, lo primero que creo es el archivo de descripción del paquete, un módulo con un esquema y un "generador de modelos" genérico:

{
  "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
};

Dicho generador de modelos tiene sus inconvenientes:hay elementos que pueden necesitar adjuntarse al modelo y tendría sentido colocarlos en el mismo módulo donde se crea el esquema. Entonces, encontrar una forma genérica de agregarlos es un poco complicado. Por ejemplo, un módulo podría exportar acciones posteriores para que se ejecuten automáticamente cuando se genera un modelo para una conexión determinada, etc. (piratería).

Ahora vamos a burlarnos de la API. Lo mantendré simple y solo me burlaré de lo que necesito para las pruebas en cuestión. Es esencial que me gustaría simular la API en general, no métodos individuales de instancias individuales. Este último podría ser útil en algunos casos, o cuando nada más ayude, pero necesitaría tener acceso a los objetos creados dentro de mi lógica comercial (a menos que se inyecten o proporcionen a través de algún patrón de fábrica), y esto significaría modificar la fuente principal. Al mismo tiempo, burlarse de la API en un solo lugar tiene un inconveniente:es una solución genérica, que probablemente implementaría una ejecución exitosa. Para probar casos de error, podría ser necesario simular instancias en las propias pruebas, pero luego, dentro de su lógica comercial, es posible que no tenga acceso directo a la instancia de, p. post creado en lo más profundo.

Entonces, echemos un vistazo al caso general de burlarse de una llamada API exitosa:

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;

Por lo general, siempre que los modelos se creen después modificando la mangosta, es posible que los simulacros anteriores se realicen por prueba para simular cualquier comportamiento. ¡Sin embargo, asegúrese de volver al comportamiento original antes de cada prueba!

Finalmente, así es como podrían verse nuestras pruebas para todas las posibles operaciones de guardado de datos. Atención, no son específicos de nuestra Post modelo y podría hacerse para todos los demás modelos con exactamente el mismo simulacro en su lugar.

// 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 esencial tener en cuenta que todavía estamos probando la funcionalidad de muy bajo nivel, pero podemos usar este mismo enfoque para probar cualquier lógica comercial que use Post.create o post.save internamente.

La parte final, hagamos las pruebas:

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

Debo decir que no es divertido hacerlo de esa manera. Pero de esta manera es realmente una prueba unitaria pura de la lógica empresarial sin ninguna base de datos en memoria o real y bastante genérica.