Unit testing with Bookshelf.js and knex.js
Asked Answered
W

2

24

I'm relatively new to Node and am working on a project using knex and bookshelf. I'm having a little bit of trouble unit testing my code and I'm not sure what I'm doing wrong.

Basically I have a model (called VorcuProduct) that looks like this:

var VorcuProduct = bs.Model.extend({
    tableName: 'vorcu_products'
});

module.exports.VorcuProduct = VorcuProduct

And a function that saves a VorcuProduct if it does not exist on the DB. Quite simple. The function doing this looks like this:

function subscribeToUpdates(productInformation, callback) {
  model.VorcuProduct
    .where({product_id: productInformation.product_id, store_id: productInformation.store_id})
    .fetch()
    .then(function(existing_model) {
        if (existing_model == undefined) {
            new model.VorcuProduct(productInformation)
                .save()
                .then(function(new_model) { callback(null, new_model)})
                .catch(callback);
        } else {
            callback(null, existing_model)
        }
    })
}

Which is the correct way to test this without hitting the DB? Do I need to mock fetch to return a model or undefined (depending on the test) and then do the same with save? Should I use rewire for this?

As you can see I'm a little bit lost, so any help will be appreciated.

Thanks!

Wrinkle answered 8/1, 2015 at 12:53 Comment(0)
F
23

I have been using in-memory Sqlite3 databases for automated testing with great success. My tests take 10 to 15 minutes to run against MySQL, but only 30 seconds or so with an in-memory sqlite3 database. Use :memory: for your connection string to utilize this technique.

A note about unit tesing - This is not true unit testing, since we're still running a query against a database. This is technically integration testing, however it runs within a reasonable time period and if you have a query-heavy application (like mine) then this technique is going to prove more effective at catching bugs than unit testing anyway.

Gotchas - Knex/Bookshelf initializes the connection at the start of the application, which means that you keep the context between tests. I would recommend writing a schema create/destroy script so that you and build and destroy the tables for each test. Also, Sqlite3 is less sensitive about foreign key constraints than MySQL or PostgreSQL, so make sure you run your app against one of those every now and then to ensure that your constraints will work properly.

Furfuraceous answered 23/9, 2015 at 21:14 Comment(3)
Thanks for sharing your experience. Out of curiosity, how many tests are you running? Also, does setup involve loading a significant amount of seed data?Subjective
@Subjective I run about 70 scenarios with around 1,000 cucumber steps in total. I set up and tear down 60 tables on each scenario. With sqlite in-memory, it takes less than half a second to do that.Furfuraceous
it has some downsides. sqlite3 for example doesnt support the jsonb data type.Bouse
S
4

This is actually a great question which brings up both the value and limitations of unit testing.

In this particular case the non-stubbed logic is pretty simple -- just a simple if block, so it's arguable whether it's this is worth the unit testing effort, so the accepted answer is a good one and points out the value of small scale integration testing.

On the other hand the exercise of doing unit testing is still valuable in that it points out opportunities for code improvements. In general if the tests are too complicated, the underlying code can probably use some refactoring. In this case a doesProductExist function can likely be refactored out. Returning the promises from knex/bookshelf instead of converting to callbacks would also be a helpful simplification.

But for comparison here's my take on what true unit-testing of the existing code would look like:

var rewire = require('rewire');
var sinon = require('sinon');
var expect = require('chai').expect;
var Promise = require('bluebird');
var subscribeToUpdatesModule = rewire('./service/subscribe_to_updates_module');

var subscribeToUpdates = subscribeToUpdatesModule.__get__(subscribeToUpdates);

describe('subscribeToUpdates', function () {
  before(function () {
    var self = this;
    this.sandbox = sinon.sandbox.create();
    var VorcuProduct = subscribeToUpdatesModule.__get__('model').VorcuProduct;

    this.saveStub = this.sandbox.stub(VorcuProduct.prototype, 'save');
    this.saveStub.returns(this.saveResultPromise);

    this.fetchStub = this.sandbox.stub()
    this.fetchStub.returns(this.fetchResultPromise);

    this.sandbox.stub(VorcuProduct, 'where', function () {
      return { fetch: self.fetchStub };
    })

  });

  afterEach(function () {
    this.sandbox.restore();
  });

  it('calls save when fetch of existing_model succeeds', function (done) {
    var self = this;
    this.fetchResultPromise = Promise.resolve('valid result');
    this.saveResultPromise = Promise.resolve('save result');
    var callback = function (err, result) {
      expect(err).to.be.null;
      expect(self.saveStub).to.be.called;
      expect(result).to.equal('save result');
      done();
    };
    subscribeToUpdates({}, callback);
  });

  // ... more it(...) blocks

});
Shadbush answered 20/11, 2016 at 20:13 Comment(1)
Great answer. Sometimes DB is not an option for lot of tests. I use jest.spyOn() and .mockImplementation() to spy and reimplement the prototype methods of Bookshelf model classes. So you can resolve any data and verify they have been called with parameters.Hydracid

© 2022 - 2024 — McMap. All rights reserved.