首页 文章

模拟/存根Mongoose模型保存方法

提问于
浏览
14

给出一个简单的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. 我've tried increasing the timeout to 20 seconds but that didn'解决任何问题 .

接下来,我扔掉了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();
    });
  });
});

这个测试通过了,但它不知何故对于存根, Mock ,你有什么新鲜......,而且'm not sure if this is the right way to go. I'在 post 上存在 save 方法,然后我明显地称它为...而且,我可以我似乎得到了非顽固的Mongoose方法将返回的参数 . 我想将 post 变量与 save 方法返回的内容进行比较,就像我在第一次测试数据时所做的那样 . 我已经尝试了couple methods但是他们都觉得非常hackish . 必须有一个干净的方式,不是吗?

几个问题:

  • 我是否真的应该避免像我一直到处读到的那样点击数据库?我的第一个例子运行正常,我可以在每次运行后清除数据库 . 但是,它对我来说并不合适 .

  • 如何从Mongoose模型中保存save方法并确保它实际测试我要测试的内容:将新对象保存到db .

2 回答

  • 39

    The basics

    在单元测试中,不应该打到DB . 我可以想到一个例外:命中内存数据库,但即使这已经存在于集成测试领域,因为你只需要在内存中保存的状态用于复杂的进程(因此不是真正的功能单元) . 所以,是的,没有实际的DB .

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

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

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

    The reality of mongoose

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

    对于您的特定示例,我使用完整的包创建了a public Git repository,但我将在答案中发布所有元素 .

    The solution

    就个人而言,我发现使用mongoose的“推荐”方式非常不可用:模型通常在模块中创建,其中定义了相应的模式,但它们已经需要连接 . 为了在同一个项目中有多个连接与完全不同的mongodb数据库进行通信,并且出于测试目的,这会让生活变得非常困难 . 事实上,只要问题完全分开,至少对我而言,猫鼬几乎无法使用 .

    所以我创建的第一件事是包描述文件,一个带有模式的模块和一个通用的“模型生成器”:

    package.json

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

    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

    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有一个缺点:它是一个通用的解决方案,可能会成功执行 . 为了测试错误情况,可能需要在测试中自己进行模拟,但是在您的业务逻辑中,您可能无法直接访问例如内心深处创造了 post .

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

    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;
    

    通常,只要在修改mongoose之后创建模型,就可以想到上面的模拟是在每个测试的基础上完成的,以模拟任何行为 . 但是,确保在每次测试之前恢复原始行为!

    最后,这就是我们对所有可能的数据保存操作的测试结果 . 请注意,这些并非特定于我们的 Post 模型,并且可以针对具有完全相同模拟的所有其他模型完成 .

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

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

    〜/ 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)
    

    我必须说,这样做并不好玩 . 但是这样,它实际上是对业务逻辑的纯单元测试,没有任何内存或真实数据库,而且相当通用 .

  • 7

    如果你想要的是某些Mongoose模型的测试 static'smethod's ,我建议你使用sinonsinon-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上找到工作(和简单)示例 .

相关问题