Метод снукера/моделирование модели Mongoose

Учитывая простую модель Мангуста:

import mongoose, { Schema } from 'mongoose';

const PostSchema = Schema({
  title:    { type: String },
  postDate: { type: Date, default: Date.now }
}, { timestamps: true });

const Post = mongoose.model('Post', PostSchema);

export default Post;

Я хочу протестировать эту модель, но я поражу несколько препятствий.

Моя текущая спецификация выглядит примерно так (некоторые вещи опущены для краткости):

import mongoose from 'mongoose';
import { expect } from 'chai';
import { Post } from '../../app/models';

describe('Post', () => {
  beforeEach((done) => {
    mongoose.connect('mongodb://localhost/node-test');
    done();
  });

  describe('Given a valid post', () => {
    it('should create the post', (done) => {
      const post = new Post({
        title: 'My test post',
        postDate: Date.now()
      });

      post.save((err, doc) => {
        expect(doc.title).to.equal(post.title)
        expect(doc.postDate).to.equal(post.postDate);
        done();
      });
    });
  });
});

Однако с этим я ударяю свою базу данных каждый раз, когда запускаю тест, чего я бы предпочел избежать.

Я пробовал использовать Mockgoose, но тогда мой тест не будет запущен.

import mockgoose from 'mockgoose';
// in before or beforeEach
mockgoose(mongoose);

Тест застрял и выдает сообщение об ошибке: Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. Я попытался увеличить таймаут до 20 секунд, но это ничего не решило.

Затем я выбросил Mockgoose и попытался использовать Sinon, чтобы заглушить вызов save.

describe('Given a valid post', () => {
  it('should create the post', (done) => {
    const post = new Post({
      title: 'My test post',
      postDate: Date.now()
    });

    const stub = sinon.stub(post, 'save', function(cb) { cb(null) })
    post.save((err, post) => {
      expect(stub).to.have.been.called;
      done();
    });
  });
});

Этот тест проходит, но это как-то не имеет для меня никакого смысла. Я совершенно новичок в обучении, насмешливости, что у вас есть, и я не уверен, что это правильный путь. Я обрезаю метод save на post, а затем я утверждаю, что он был вызван, но я, очевидно, его называю... Кроме того, я не могу найти аргументы, -stubbed Mongoose метод вернется. Я хотел бы сравнить переменную post с тем, что возвращает метод save, как в самом первом тесте, в котором я попал в базу данных. Я пробовал пару методов , но все они чувствуют себя довольно хаки. Должен быть чистый путь, нет?

Пара вопросов:

  • Должен ли я действительно избегать попадания в базу данных, как я всегда читал везде? Мой первый пример отлично работает, и я могу очистить базу данных после каждого запуска. Однако мне это действительно не нравится.

  • Как я закрою метод сохранения из модели Mongoose и убедитесь, что он фактически проверяет, что я хочу проверить: сохранение нового объекта в db.

Ответ 1

Основы

В модульном тестировании не следует ударять по БД. Я мог бы подумать об одном исключении: попадание в БД памяти, но даже это уже находится в области интеграционного тестирования, поскольку вам потребуется только состояние, сохраненное в памяти для сложных процессов (и, следовательно, не действительно единицы функциональности). Итак, да нет фактической БД.

Что вы хотите протестировать в модульных тестах, так это то, что ваша бизнес-логика приводит к правильным вызовам API на интерфейсе между вашим приложением и БД. Вы можете и, вероятно, должны предположить, что разработчики DB API/драйвера провели хорошую проверку работы, что все под API ведет себя так, как ожидалось. Тем не менее, вы также хотите осветить в своих тестах, как ваша бизнес-логика реагирует на различные достоверные результаты API, такие как успешные сбережения, сбои из-за согласованности данных, сбои из-за проблем подключения и т.д.

Это означает, что то, что вам нужно и которое нужно высмеять, - это все, что находится ниже интерфейса драйвера DB. Однако вам нужно будет моделировать это поведение, чтобы ваша бизнес-логика могла быть протестирована для всех результатов вызовов БД.

Легче сказать, чем сделать, потому что это означает, что вам нужно иметь доступ к API через технологию, которую вы используете, и вам нужно знать API.

Реальность мангуста

Придерживаясь основ, мы хотим издеваться над вызовами, выполняемыми базовым "драйвером", который использует mongoose. Предполагая, что это node-mongodb-native, нам нужно издеваться над этими вызовами. Понимание полного взаимодействия между mongoose и родным драйвером непросто, но обычно это сводится к методам в mongoose.Collection, потому что последний расширяет mongoldb.Collection и не переопределяет такие методы, как insert. Если мы можем контролировать поведение insert в этом конкретном случае, то мы знаем, что мы издеваемся над доступом к БД на уровне API. Вы можете проследить его в источнике обоих проектов, что Collection.insert - действительно собственный метод драйвера.

В вашем конкретном примере я создал общедоступный репозиторий Git с полным пакетом, но я разместим все элементы здесь ответ.

Решение

Лично я считаю, что "рекомендуемый" способ работы с mongoose совершенно неприменим: модели обычно создаются в модулях, где определены соответствующие схемы, но они уже нуждаются в соединении. Для того, чтобы иметь несколько соединений для разговора с совершенно разными базами данных mongodb в одном и том же проекте и для целей тестирования, это делает жизнь очень трудной. На самом деле, как только проблемы полностью разделены, мангуст, по крайней мере для меня, становится почти непригодным.

Итак, первое, что я создаю, это файл описания пакета, модуль со схемой и общий "генератор модели":

package.json

{
  "name": "xxx",
  "version": "0.1.0",
  "private": true,
  "main": "./src",
  "scripts": {
    "test" : "mocha --recursive"
  },
  "dependencies": {
    "mongoose": "*"
  },
  "devDependencies": {
    "mocha": "*",
    "chai": "*"
  }
}

ЦСИ/post.js

var mongoose = require("mongoose");

var PostSchema = new mongoose.Schema({
    title: { type: String },
    postDate: { type: Date, default: Date.now }
}, {
    timestamps: true
});

module.exports = PostSchema;

ЦСИ/index.js

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

Такой генератор модели имеет свои недостатки: есть элементы, которые, возможно, необходимо привязать к модели, и было бы целесообразно разместить их в том же модуле, где создана схема. Поэтому найти общий способ добавить их немного сложно. Например, модуль может экспортировать пост-действия для автоматического запуска, когда модель создается для данного соединения и т.д. (Взлома).

Теперь давайте издеваемся над API. Я буду держать его простым и будет только издеваться над тем, что мне нужно для этих тестов. Очень важно, чтобы я хотел издеваться над API в целом, а не индивидуальными методами отдельных экземпляров. Последнее может быть полезно в некоторых случаях или когда ничего больше не помогает, но мне нужно будет иметь доступ к объектам, созданным внутри моей бизнес-логики (если они не введены или не предоставлены через некоторый шаблон factory), и это будет означать изменение основного источник. В то же время издевательский API в одном месте имеет недостаток: это общее решение, которое, вероятно, будет успешно выполнять. Для тестирования случаев ошибок может потребоваться насмешка в случаях в самих тестах, но тогда в вашей бизнес-логике у вас может не быть прямого доступа к экземпляру, например. post создан глубоко внутри.

Итак, давайте взглянем на общий случай издевательского успешного вызова API:

тест/mock.js

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;

Как правило, до тех пор, пока модели создаются после изменения мангуста, можно подумать, что вышеупомянутые макеты выполняются на каждой тестовой основе для имитации любого поведения. Однако перед каждым тестированием обязательно вернитесь к исходному поведению!

Наконец, так выглядят наши тесты для всех возможных операций сохранения данных. Обратите внимание, что это не относится к нашей модели post и может быть сделано для всех других моделей с точно таким же макетом.

тест/test_model.js

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

});

Важно отметить, что мы по-прежнему проверяем функциональность самого низкого уровня, но мы можем использовать этот же подход для тестирования любой бизнес-логики, которая использует внутри себя Post.create или post.save.

Самый последний бит, пусть запускают тесты:

~/source/web/xxx $npm test

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

Я должен сказать, что это не забавно делать так. Но таким образом это действительно чисто модульное тестирование бизнес-логики без каких-либо встроенных или реальных БД и довольно общий.

Ответ 2

Если вы хотите проверить static's и method's определенной модели Mongoose, я бы рекомендовал вам использовать sinon и sinon-mongoose. (Я думаю, он совместим с chai)

Таким образом, вам не нужно подключаться к Mongo DB.

Следуя вашему примеру, предположим, что у вас есть статический метод findLast

//If you are using callbacks
PostSchema.static('findLast', function (n, callback) {
  this.find().limit(n).sort('-postDate').exec(callback);
});

//If you are using Promises
PostSchema.static('findLast', function (n) {
  this.find().limit(n).sort('-postDate').exec();
});

Затем, чтобы проверить этот метод

var Post = mongoose.model('Post');
// If you are using callbacks, use yields so your callback will be called
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .yields(null, 'SUCCESS!');

Post.findLast(10, function (err, res) {
  assert(res, 'SUCCESS!');
});

// If you are using Promises, use 'resolves' (using sinon-as-promised npm) 
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .resolves('SUCCESS!');

Post.findLast(10).then(function (res) {
  assert(res, 'SUCCESS!');
});

Вы можете найти рабочие (и простые) примеры в sinon-mongoose repo.