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

我试图testing一个服务函数,我用它来保存一个使用Mongoose模型的小部件。 我想在我的模型上存储保存实例方法,但是我找不出一个好的解决scheme。 我看到了其他的build议,但没有一个看起来完整。

看… 这个 , 这个 。

这是我的模特

// 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数据,创build一个新的小部件,然后使用“保存”实例方法保存小部件。 然后根据保存调用的结果调用回传错误和文档。

我只想testing,当我打电话给createWidget({title:'Widget A'})…

  • Widget构造函数被传递给服务函数的数据被调用一次
  • 新创build的小部件对象上的保存实例方法被调用一次
  • EXTRA CREDIT:保存实例方法用err调用返回值为null,为doc调用{title:'Widget A'}。

为了单独testing这个,我可能需要…

  • 模拟或存根控件的构造函数,以便它返回一个模拟的小部件对象,我创build的testing的一部分。
  • 存根模拟小部件对象的保存function,所以我可以控制发生的事情。

我很难找出如何与Sinon做到这一点。 我已经尝试了在SO的页面上发现的几个变化,没有运气。

笔记:

  • 我不想将已经构build好的模型对象传递给服务,因为我希望服务是“知道”mongoose的唯一的东西。
  • 我知道这不是最大的交易(只是用更多的集成或端到端testing来testing这个,但是找出解决scheme是很好的。

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

如果要testing的话,这是我将如何处理它,首先有一种方法来注入我的模拟小部件到小部件服务。 我知道有节点劫持 , 嘲弄或类似node-di的东西,它们都有不同的风格,我相信还有更多。 select一个并使用它。

一旦我得到了正确的,然后我用我的模拟小部件模块创build我的小部件服务。 然后我做这样的事情(这是使用摩卡 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(); }); }); 

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

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

这是我该怎么做的。 我正在使用Mockery来操纵模块加载。 widgetservice.js的代码必须改变才能调用require('./widget'); ,没有.js扩展名。 如果没有修改,下面的代码将不起作用,因为我使用避免require调用扩展的一般build议的做法。 嘲笑明确指出传递给require调用的名字必须完全匹配。

testing者是摩卡 。

代码如下。 我已经在代码本身提出了丰富的评论。

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

除非我遗漏了一些东西,否则这个问题涵盖了问题中提到的所有testing。

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

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

然后testing方法(使用摩卡)

 // 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 。