Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stub save Instance Method of Mongoose Model With Sinon

I am trying to test a service function I use to save a widget using a Mongoose model. I want to stub out the save instance method on my model, but I cannot figure out a good solution. I have seen other suggestions, but none seem to be complete.

See... this, and this.

Here is my model...

// widget.js

var mongoose = require('mongoose');

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

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

module.exports = Widget;

Here is my service...

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

};

My service is very simple. It accepts some JSON data, creates a new widget, and then saves the widget using the "save" instance method. It then calls back passing an err and doc based on the outcome of the save call.

I only want to test that when I call createWidget({title: 'Widget A'})...

  • The Widget constructor is called once with the data I passed to the service function
  • The save instance method on the newly created widget object is called once
  • EXTRA CREDIT: That the save instance method calls back with null for the err and with {title: 'Widget A'} for the doc.

In order to test this in isolation, I would probably need to...

  • Mock or stub the Widget constructor so that it would return a mock widget object that I create as part of my test.
  • Stub the mock widget object's save function so I can control what occurs.

I am having trouble figuring out how to do this with Sinon. I have tried several variations found on the pages of SO with no luck.

NOTES:

  • I don't want to pass in an already constructed model object to the service because I want the service to be the only thing that "knows" about mongoose.
  • I know this is not the biggest deal (to just test this with more of an integration or end-to-end test, but it would be nice to figure out a solution.

Thanks for any help you can provide.

like image 879
Kevin Avatar asked Mar 12 '14 01:03

Kevin


3 Answers

If were to test that, this is how I would approach it, first have a way to inject my mocked widget to the widget-service. I know there's node-hijack, mockery or something like node-di, they all have different styles, I'm sure there's more. Choose one and use it.

Once I get that right, then I create my widget-service with my mock widget module. Then I do something like this(this is using 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();
    });
});

This is just a sample, my tests sometimes get more complicated. It happens that most of the time, the backend calling function(here it is, widget.save) that I want to mock, is the one that I want it's behavior to change with every different test, so that's why I reset the stub everytime.

Here's also another example for doing similar thing: https://github.com/mozilla-b2g/gaia/blob/16b7f7c8d313917517ec834dbda05db117ec141c/apps/sms/test/unit/thread_ui_test.js#L1614

like image 185
Farid Nouri Neshat Avatar answered Oct 19 '22 11:10

Farid Nouri Neshat


Here is how I would do it. I'm using Mockery to manipulate the module loading. The code of widgetservice.js must changed so that it calls require('./widget');, without the .js extension. Without the modification, the following code won't work because I use the general recommended practice of avoiding extensions in require calls. Mockery is states clearly that the names passed to the require call must match exactly so.

The test runner is Mocha.

The code follows. I've put copious comments in the code itself.

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

Unless I've missed something, this covers all the tests that were mentioned in the question.

like image 3
Louis Avatar answered Oct 19 '22 11:10

Louis


With current version of Mongoose you can use create method

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

var createWidget = function(data, callback) {
  Widget.create(data, callback);
};

Then to test the method (using 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();
      });
    });
  });
});

Also, if you want to mock chained methods use sinon-mongoose.

like image 3
Gon Avatar answered Oct 19 '22 10:10

Gon