模拟/存根猫鼬模型保存方法 [英] Mocking/stubbing Mongoose model save method

查看:74
本文介绍了模拟/存根猫鼬模型保存方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给出一个简单的猫鼬模型:

Given a simple Mongoose model:

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;

我希望测试该模型,但遇到了一些障碍.

I wish to test this model, but I'm hitting a few roadblocks.

我当前的规格看起来像这样(为简洁起见,省略了一些内容):

My current spec looks something like this (some stuff omitted for brevity):

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

但是,每次运行测试时,我都会访问数据库,这是我希望避免的.

However, with this I'm hitting my database every time I run the test, which I would prefer to avoid.

我已经尝试过使用 Mockgoose ,但是后来我的测试无法运行.

I've tried using Mockgoose, but then my test won't run.

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秒,但这并不能解决任何问题.

The test gets stuck and throws an error saying: Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. I've tried increasing the timeout to 20 seconds but that didn't solve anything.

接下来,我扔掉了Mockgoose,并尝试使用Sinon来打断save呼叫.

Next, I threw away Mockgoose and tried using Sinon to stub the save call.

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

该测试通过了,但是对我来说并没有多大意义.我对存根,嘲笑,您有什么感到很陌生,...我不确定这是否是正确的方法.我在post上插入了save方法,然后断言它已被调用,但是我显然在调用它...而且,我似乎无法获得非参数-stubbed的Mongoose方法将返回.我想将post变量与save方法返回的内容进行比较,例如在我打数据库的第一个测试中.我尝试了夫妇 ="https://stackoverflow.com/questions/11318972/stubbing-a-mongoose-model-with-sinon">方法,但它们都让人感到有些草草.必须有一种干净的方法,不是吗?

This test passes, but it somehow doesn't make much sense to me. I'm quite new to stubbing, mocking, what have you, ... and I'm not sure if this is the right way to go. I'm stubbing the save method on post, and then I'm asserting it to have been called, but I'm obviously calling it... Also, I can't seem to get to the arguments the non-stubbed Mongoose method would return. I would like to compare the post variable to something the save method returns, like in the very first test where I hit the database. I've tried a couple of methods but they all feel quite hackish. There must be a clean way, no?

问题对:

  • 我真的应该避免像往常一样读取数据库吗?我的第一个示例运行良好,每次运行后都可以清除数据库.但是,这对我来说真的不对.

  • Should I indeed avoid hitting the database like I've always read everywhere? My first example works fine and I could clear the database after each run. However, it doesn't really feel right to me.

我该如何从Mongoose模型中存入save方法,并确保它实际测试了我要测试的内容:将一个新对象保存到数据库中.

How would I stub the save method from the Mongoose model and make sure it actually tests what I want to test: saving a new object to the db.

推荐答案

基础知识

在单元测试中,不应击中数据库.我可以想到一个例外:访问内存中的数据库,但是即使那样也已经位于集成测试领域,因为您只需要将状态保存在内存中即可用于复杂的流程(因此实际上不需要功能单元).因此,是的,没有实际的数据库.

In unit testing one should not hit the DB. I could think of one exception: hitting an in-memory DB, but even that lies already in the area of integration testing as you would only need the state saved in memory for complex processes (and thus not really units of functionality). So, yes no actual DB.

您要在单元测试中进行测试的是,您的业务逻辑会导致在应用程序和数据库之间的接口上进行正确的API调用.您可以并且可能应该假设DB API/驱动程序开发人员已经很好地测试了API之下的所有内容都按预期运行.但是,您还想在测试中涵盖业务逻辑如何对不同的有效API结果做出反应,例如成功保存,由于数据一致性导致的失败,由于连接问题导致的失败等.

What you want to test in unit tests is that your business logic results in correct API calls at the interface between your application and the DB. You can and probably should assume that the DB API/driver developers have done a good job testing that everything below the API behaves as expected. However, you also want to cover in your tests how your business logic reacts to different valid API results such as successful saves, failures due to data consistency, failures due to connection issues etc.

这意味着您需要和想要模拟的是数据库驱动程序界面下的所有内容.但是,您需要对该行为进行建模,以便可以针对DB调用的所有结果测试您的业务逻辑.

This means that what you need and want to mock is everything that is below the DB driver interface. You would, however, need to model that behaviour so that your business logic can be tested for all outcomes of the DB calls.

说起来容易做起来难,因为这意味着您需要通过使用的技术访问API,并且需要了解API.

Easier said than done because this means you need to have access to the API via the technology you use and you need to know the API.

猫鼬的现实

坚持基本原则,我们想模拟猫鼬使用的底层驱动程序"执行的调用.假设它是 node-mongodb-native ,我们需要模拟这些调用.理解猫鼬和本机驱动程序之间的完全相互作用并不容易,但是通常归结为mongoose.Collection中的方法,因为后者扩展了mongoldb.Collection并且insert这样的重新实现方法.如果我们能够在这种特殊情况下控制insert的行为,那么我们知道我们在API级别上模拟了数据库访问.您可以在两个项目的源代码中找到它,Collection.insert实际上是本机驱动程序方法.

Sticking to the basics we want to mock the calls performed by the underlying 'driver' that mongoose uses. Assuming it is node-mongodb-native we need to mock out those calls. Understanding the full interplay between mongoose and the native driver is not easy, but it generally comes down to the methods in mongoose.Collection because the latter extends mongoldb.Collection and does not reimplement methods like insert. If we are able to control the behaviour of insert in this particular case, then we know we mocked out the DB access at the API level. You can trace it in the source of both projects, that Collection.insert is really the native driver method.

对于您的特定示例,我使用完整的软件包创建了公共Git存储库 ,但我将在此处将所有元素发布在答案中.

For your particular example I created a public Git repository with a complete package, but I will post all of the elements here in the answer.

解决方案

我个人觉得使用猫鼬"的推荐"方法非常不实用:模型通常在定义了相应模式的模块中创建,但是它们已经需要连接.为了拥有多个连接以便与同一项目中的完全不同的mongodb数据库进行对话以及出于测试目的,这使生活变得非常艰难.实际上,一旦关注完全分离,猫鼬就变得几乎无法使用,至少对我而言.

Personally I find the "recommended" way of working with mongoose quite unusable: models are usually created in the modules where the corresponding schemas are defined, yet they already need a connection. For purposes of having multiple connections to talk to completely different mongodb databases in the same project and for testing purposes this makes life really hard. In fact, as soon as concerns are fully separated mongoose, at least to me, becomes nearly unusable.

因此,我创建的第一件事是程序包描述文件,一个具有模式的模块和一个通用的模型生成器":

So the first thing I create is the package description file, a module with a schema and a generic "model generator":

package.json

package.json

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

src/post.js

src/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;

src/index.js

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

这种模型生成器有其缺点:有些元素可能需要附加到模型上,将它们放置在创建模式的同一模块中是有意义的.因此,找到一种通用的添加方式有些棘手.例如,当为给定的连接等生成模型时,模块可以导出后操作以自动运行(黑客).

Such a model generator has its drawbacks: there are elements that may need to be attached to the model and it would make sense to place them in the same module where the schema is created. So finding a generic way to add those is a bit tricky. For example, a module could export post-actions to be automatically run when a model is generated for a given connection etc. (hacking).

现在让我们模拟一下API.我会保持简单,并且只会嘲笑我所需要的测试.我必须总体上模拟API,而不是单个实例的单个方法,这一点很重要.后者在某些情况下或在没有其他帮助的情况下可能很有用,但是我需要访问在我的业务逻辑内部创建的对象(除非通过某种工厂模式注入或提供),这意味着修改主源.同时,在一处模拟该API有一个缺点:这是一个通用解决方案,可能会实现成功的执行.对于测试错误案例,可能需要在测试本身的实例中进行模拟,但是在您的业务逻辑中,您可能无法直接访问例如post创建于内部深处.

Now let's mock the API. I'll keep it simple and will only mock what I need for the tests in question. It is essential that I would like to mock out the API in general, not individual methods of individual instances. The latter might be useful in some cases, or when nothing else helps, but I would need to have access to objects created inside of my business logic (unless injected or provided via some factory pattern), and this would mean modifying the main source. At the same time, mocking the API in one place has a drawback: it is a generic solution, which would probably implement successful execution. For testing error cases, mocking in instances in the tests themselves could be required, but then within your business logic you might not have direct access to the instance of e.g. post created deep inside.

因此,让我们看一下模拟成功的API调用的一般情况:

So, let's have a look at the general case of mocking successful API call:

test/mock.js

test/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;

通常,只要在修改猫鼬后 创建模型,可以认为上述模拟是在每个测试的基础上完成的,以模拟任何行为.但是,在每次测试之前,请务必还原为原始行为!

Generally, as long as models are created after modifying mongoose, it is thinkable that the above mocks are done on per test basis to simulate any behaviour. Make sure to revert to the original behaviour, however, before every test!

最后,这就是我们对所有可能的数据保存操作进行的测试的样子.请注意,这些不是我们的Post模型所特有的,并且可以在其他所有具有完全相同的模拟的模型上完成.

Finally this is how our tests for all possible data saving operations could look like. Pay attention, these are not specific to our Post model and could be done for all other models with exactly the same mock in place.

test/test_model.js

test/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.createpost.save的任何业务逻辑.

It is essential to note that we are still testing the very low level functionality, but we can use this same approach for testing any business logic that uses Post.create or post.save internally.

最后一点,让我们运行测试:

The very final bit, let's run the tests:

〜/source/web/xxx $ npm测试

~/source/web/xxx $ npm test

> xxx@0.1.0 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)

我必须说,这样做不是一件有趣的事情.但是这样一来,它实际上就是业务逻辑的纯单元测试,而没有任何内存或真实的数据库,而且相当通用.

I must say, this is no fun to do it that way. But this way it is really pure unit-testing of the business logic without any in-memory or real DBs and fairly generic.

这篇关于模拟/存根猫鼬模型保存方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆