首页 文章

用Sinon保存Mongoose模型的实例方法

提问于
浏览
10

我正在尝试使用Mongoose模型测试我用来保存小部件的服务功能 . 我想在我的模型上存根保存实例方法,但我无法找到一个好的解决方案 . 我见过其他建议,但似乎都没有完整 .

见...... thisthis .

这是我的模特......

// widget.js

var mongoose = require('mongoose');

var widgetSchema = mongoose.Schema({
    title: {type: String, default: ''}
});

var Widget = mongoose.model('Widget',  widgetSchema);

module.exports = Widget;

这是我的服务......

// widgetservice.js

var Widget = require('./widget.js');

var createWidget = function(data, callback) {

    var widget = new Widget(data);
    widget.save(function(err, doc) {
        callback(err, doc);
    });

};

我的服务很简单 . 它接受一些JSON数据,创建一个新的小部件,然后使用“save”实例方法保存小部件 . 然后它根据保存调用的结果回调传递错误和doc .

当我调用createWidget时,我只想测试它({title:'Widget A'})...

  • 使用传递给服务函数的数据调用Widget构造函数一次

  • 新创建的窗口小部件对象上的save实例方法被调用一次

  • EXTRA CREDIT:保存实例方法为错误调用null,为文档调用{title:'Widget A'} .

为了单独测试这个,我可能需要......

  • 模拟或存根Widget构造函数,以便它返回我作为测试的一部分创建的模拟小部件对象 .

  • 存储模拟小部件对象的保存功能,以便我可以控制发生的事情 .

我无法弄清楚如何用Sinon做到这一点 . 我已经尝试过在SO页面上找到的几个变种而没有运气 .

笔记:

  • 我不想将已构造的模型对象传递给服务,因为我希望服务是"knows"关于mongoose的唯一内容 .

  • 我知道这不是最大的交易(只是通过更多的集成或端到端测试来测试它,但是找出解决方案会很好 .

感谢您的任何帮助,您可以提供 .

3 回答

  • 3

    如果要测试它,这就是我接近它的方法,首先要有一种方法将我的模拟小部件注入到widget-service中 . 我知道有node-hijackmockery或类似于node-di的东西,它们都有不同的风格,我更多 . 选择一个并使用它 .

    一旦我做对了,然后我用我的模拟小部件模块创建我的小部件服务 . 然后我做这样的事情(这是使用mocha顺便说一句):

    // Either do this:
    saveStub = sinon.stub();
    function WidgetMock(data) {
        // some mocking stuff
        // ...
        // Now add my mocked stub.
        this.save = saveStub;
    }
    
    
    // or do this:
    WidgetMock = require('./mocked-widget');
    var saveStub = sinon.stub(WidgetMock.prototype, 'save');
    
    
    diInject('widget', WidgetMock); // This function doesn't really exists, but it should
    // inject your mocked module instead of real one.
    
    beforeEach(function () {
        saveStub.reset(); // we do this, so everytime, when we can set the stub only for
        // that test, and wouldn't clash with other tests. Don't do it, if you want to set
        // the stub only one time for all.
    });
    after(function () {
        saveStub.restore();// Generally you don't need this, but I've seen at times, mocked
        // objects clashing with other mocked objects. Make sure you do it when your mock
        // object maybe mocked somewhere other than this test case.
    });
    it('createWidget()', function (done) {
        saveStub.yields(null, { someProperty : true }); // Tell your stub to do what you want it to do.
        createWidget({}, function (err, result) {
            assert(!err);
            assert(result.someProperty);
            sinon.assert.called(saveStub); // Maybe do something more complicated. You can
            // also use sinon.mock instead of stubs if you wanna assert it.
            done();
        });
    });
    it('createWidget(badInput)', function (done) {
        saveStub.yields(new Error('shhoo'));
        createWidget({}, function (err, result) {
            assert(err);
            done();
        });
    });
    

    这只是一个例子,我的测试有时会变得更复杂 . 它发生在大多数情况下,我想要模拟的后端调用函数(这里是widget.save)是我希望它的行为随着每个不同的测试而改变的,所以这就是我每次重置存根的原因 .

    这也是做类似事情的另一个例子:https://github.com/mozilla-b2g/gaia/blob/16b7f7c8d313917517ec834dbda05db117ec141c/apps/sms/test/unit/thread_ui_test.js#L1614

  • 3

    我就是这样做的 . 我正在使用Mockery来操纵模块加载 . 必须更改 widgetservice.js 的代码,以便在没有 .js 扩展名的情况下调用 require('./widget'); . 如果没有修改,以下代码将无法工作,因为我使用了避免 require 调用中的扩展的一般建议做法 . Mockery明确指出传递给 require 调用的名称必须完全匹配 .

    测试运动员是Mocha .

    代码如下 . 我在代码本身中添加了大量的注释 .

    var mockery = require("mockery");
    var sinon = require("sinon");
    
    // We grab a reference to the pristine Widget, to be used later.
    var Widget = require("./widget");
    
    // Convenience object to group the options we use for mockery.
    var mockery_options = {
        // `useCleanCache` ensures that "./widget", which we've
        // previously loaded is forgotten when we enable mockery.
        useCleanCache: true,
        // Please look at the documentation on these two options. I've
        // turned them off but by default they are on and they may help
        // with creating a test suite.
        warnOnReplace: false,
        warnOnUnregistered: false
    };
    
    describe("widgetservice", function () {
        describe("createWidget", function () {
            var test_doc = {title: "foo"};
    
            it("creates a widget with the correct data", function () {
    
                // Create a mock that provides the bare minimum.  We
                // expect it to be called with the value of `test_doc`.
                // And it returns an object which has a fake `save` method
                // that does nothing. This is *just enough* for *this*
                // test.
                var mock = sinon.mock().withArgs(test_doc)
                    .returns({"save": function () {}});
    
                // Register our mock with mockery.
                mockery.registerMock('./widget', mock);
                // Then tell mockery to intercept module loading.
                mockery.enable(mockery_options);
    
                // Now we load the service and mockery will give it our mock
                // Widget.
                var service = require("./widgetservice");
    
                service.createWidget(test_doc, function () {});
                mock.verify(); // Always remember to verify!
            });
    
            it("saves a widget with the correct data", function () {
                var mock;
    
                // This will intercept object creation requests and return an
                // object on which we can check method invocations.
                function Intercept() {
                    // Do the usual thing...
                    var ret = Widget.apply(this, arguments);
    
                    // Mock only on the `save` method. When it is called,
                    // it should call its first argument with the
                    // parameters passed to `yields`. This effectively
                    // simulates what mongoose would do when there is no
                    // error.
                    mock = sinon.mock(ret, "save").expects("save")
                        .yields(null, arguments[0]);
    
                    return ret;
                }
    
                // See the first test.
                mockery.registerMock('./widget', Intercept);
                mockery.enable(mockery_options);
    
                var service = require("./widgetservice");
    
                // We use sinon to create a callback for our test. We could
                // just as well have passed an anonymous function that contains
                // assertions to check the parameters. We expect it to be called
                // with `null, test_doc`.
                var callback = sinon.mock().withArgs(null, test_doc);
                service.createWidget(test_doc, callback);
                mock.verify();
                callback.verify();
            });
    
            afterEach(function () {
                // General cleanup after each test.
                mockery.disable();
                mockery.deregisterAll();
    
                // Make sure we leave nothing behind in the cache.
                mockery.resetCache();
            });
        });
    });
    

    除非我错过了什么,否则这涵盖了问题中提到的所有测试 .

  • 5

    使用当前版本的Mongoose,您可以使用 create 方法

    // widgetservice.js
    var Widget = require('./widget.js');
    
    var createWidget = function(data, callback) {
      Widget.create(data, callback);
    };
    

    然后测试方法(使用Mocha)

    // test.js
    var sinon = require('sinon');
    var mongoose = require('mongoose');
    var Widget = mongoose.model('Widget');
    var WidgetMock = sinon.mock(Widget);
    
    var widgetService = require('...');
    
    describe('widgetservice', function () {
    
      describe('createWidget', function () {
    
        it('should create a widget', function () {
          var doc = { title: 'foo' };
    
          WidgetMock
            .expects('create').withArgs(doc)
            .yields(null, 'RESULT');
    
          widgetService.createWidget(doc, function (err, res) {
            assert.equal(res, 'RESULT');
            WidgetMock.verify();
            WidgetMock.restore();
          });
        });
      });
    });
    

    另外,如果要模拟链式方法,请使用sinon-mongoose .

相关问题