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

时间:2014-03-12 01:51:25

标签: javascript node.js unit-testing mongoose sinon

我正在尝试使用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”实例方法保存小部件。然后它根据保存调用的结果回调传递错误和文档。

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

  • 使用传递给服务函数的数据
  • 调用Widget构造函数一次
  • 新创建的窗口小部件对象上的save实例方法被调用一次
  • EXTRA CREDIT:保存实例方法为错误调用null,为文档调用{title:'Widget A'}。

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

  • 模拟或存根Widget构造函数,以便它返回我作为测试的一部分创建的模拟小部件对象。
  • 存储模拟小部件对象的保存功能,以便我可以控制发生的事情。

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

注意:

  • 我不想将已构建的模型对象传递给服务,因为我希望服务是唯一“知道”mongoose的东西。
  • 我知道这不是最大的交易(只是通过更多的集成或端到端测试来测试它,但是找出解决方案会很好。

感谢您提供的任何帮助。

3 个答案:

答案 0 :(得分:5)

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

一旦我做到了,我就用我的模拟小部件模块创建我的小部件服务。然后我做这样的事情(这是使用mocha btw):

// 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)是我希望它的行为随着每个不同的测试而改变的,所以&#39 ;为什么我每次都重置存根。

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

答案 1 :(得分:3)

我将如何做到这一点。我正在使用Mockery来操纵模块加载。必须更改widgetservice.js的代码,以便在没有require('./widget');扩展名的情况下调用.js。如果没有修改,以下代码将无效,因为我使用了避免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();
        });
    });
});

除非我遗漏了某些内容,否则这涵盖了问题中提及的所有测试。

答案 2 :(得分:3)

使用当前版本的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