【问题标题】:Stub save Instance Method of Mongoose Model With Sinon使用Sinon的Mongoose模型的存根保存实例方法
【发布时间】:2014-04-15 22:22:25
【问题描述】:

我正在尝试使用 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 数据,创建一个新的小部件,然后使用“保存”实例方法保存小部件。然后它会根据保存调用的结果回调传递 err 和 doc。

我只想在调用 createWidget({title: 'Widget A'}) 时进行测试...

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

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

  • 模拟或存根 Widget 构造函数,以便它返回我在测试中创建的模拟小部件对象。
  • 存根模拟小部件对象的保存函数,以便我可以控制发生的情况。

我无法弄清楚如何使用诗乃做到这一点。我尝试了在 SO 页面上找到的几种变体,但都没有运气。

注意事项:

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

感谢您提供的任何帮助。

【问题讨论】:

  • “我不想将已经构建的模型对象传递给服务,因为我希望服务成为唯一“了解”猫鼬的事物。”那么你应该使用依赖注入容器,而不是像现在这样依赖 nodejs 模块来注入依赖。或者至少有一种方法可以在您的服务中覆盖 Widget 类。使用 setter 注入,我认为现在您的脚本是不可测试的。

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


【解决方案1】:

如果要测试它,这就是我的处理方法,首先有一种方法将我的模拟小部件注入小部件服务。我知道有node-hijackmockerynode-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)是我希望它的行为随着每个不同的测试而改变的函数,所以这就是我每次重置存根的原因.

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

【讨论】:

  • 那么,您会模拟整个 Widget 并将其注入到被测方法中吗?您是否需要以不同的方式重写该方法以支持这种方法?假设你的测试方法,你将如何处理这部分?你还能使用'var widget = new Widget(data)'吗?谢谢!
  • 是的,要么我只是使用真正的实现并用存根覆盖函数,要么我只是尽可能多地模拟对象以使用它。在这里,如果您只想拥有new Widget().save,您只需要一个包含save 方法的构造函数,仅此而已。我尽量保持简单。我不明白你以不同的方式重写方法是什么意思。从技术上讲,只要您的依赖注入正确,您就不必在方法级别关心它。
  • 我明白了。我接受了你的回答,这与路易斯的回答相似,因为你先回答了。谢谢!
  • @Kevin - 关于这一行 diInject('widget', WidgetMock);这是框架的一部分吗?如果是,是哪一个?
  • 在这一行 - diInject('widget', WidgetMock);什么是“小部件”?
【解决方案2】:

我会这样做。我正在使用Mockery 来操作模块加载。 widgetservice.js 的代码必须更改,以便它调用 require('./widget');,而不使用 .js 扩展名。如果不进行修改,以下代码将无法工作,因为我使用了一般推荐的做法,即在 require 调用中避免扩展。 Mockery 明确指出传递给 require 调用的名称必须完全匹配。

测试运行者是Mocha

代码如下。我在代码中加入了大量的 cmets。

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

除非我遗漏了什么,否则这涵盖了问题中提到的所有测试。

【讨论】:

  • 谢谢路易斯。我认为你的答案和第一个答案本质上是相似的,模拟和注入了 save 方法的全部新功能。我接受了他,因为他是第一个。再次感谢!
  • 这不是重复的。这里使用 sinon 模拟而不是存根。它们是好东西,有时当你想断言你的存根是否被你想要的参数调用时,你应该使用它们。虽然你不能用两个不同的参数调用它们并断言两者:github.com/cjohansen/Sinon.JS/issues/202
  • 感谢两位的帮助。如果我能接受两者,我会的。 --凯文
【解决方案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

【讨论】:

    猜你喜欢
    • 2018-06-20
    • 2018-05-31
    • 2020-06-26
    • 2017-04-27
    • 1970-01-01
    • 2021-06-10
    • 2014-07-12
    • 2015-03-06
    • 2015-04-18
    相关资源
    最近更新 更多