Monday, June 30, 2014

How to Unit Test a Sails.js Model Without Lifting Sails

The Problem

As a quick follow-up to the previous post about creating a traditional MVC-style Web API using Sails.js, I did get some questions around how to test models in Sails, especially those that have instance methods and override their Waterline lifecycle callbacks, such as afterCreate and afterUpdate.  This post is for you guys!



The Approach

First, we need to ensure that Waterline is installed as a dev dependency.  This is accomplished by going to the command line of your Sails application's root and typing in sudo npm install waterline --save-dev (or just npm install waterline --save-dev for Windows folks).

Note that this approach is confirmed to work in Sails 0.9.16, and the Waterline approach used here may not jive with 0.10.x, as some readers have reported.

Using the same exact unit testing framework (Mocha) as the last article, let's assume we fortified our Movie model with an "unbiasedCriticism" instance method and some lifecycle callbacks using Socket.io, defined as follows:

/api/constants/SocketIOConstants.js
/**
 * Events for Socket.io pub/sub
 * @type {string}
 */
exports.EVENT_MOVIE_CREATED = 'movie:created';
exports.EVENT_MOVIE_UPDATED = 'movie:updated';


/api/models/Movie.js
var SocketIOConstants = require('../constants/SocketIOConstants');

/**
 * The Movie model
 */
var Movie = {
    tableName: 'Movie',
    attributes: {
        movieId: {
            type: 'INTEGER',
            required: true,
            unique: true
        },
        name: {
            type: 'STRING',
            required: true
        },
        releaseDate: {
            type: 'DATE',
            required: true
        },
        rating: {
            type: 'STRING',
            required: true
        },
        grossEarnings: 'FLOAT',

        // Instance methods
        unbiasedCriticism: function() {
            if(this.name.toLowerCase().indexOf('godfather') > -1) {
                return 'PHENOMENAL!';
            }
            else {
                return 'It stinks!';
            }
        }
    },

    // Lifecycle callbacks
    afterCreate: function(newRecord, next) {
        sails.io.sockets.emit(SocketIOConstants.EVENT_MOVIE_CREATED, newRecord);
        next();
    },

    afterUpdate: function(updatedRecord, next) {
        sails.io.sockets.emit(SocketIOConstants.EVENT_MOVIE_UPDATED, updatedRecord);
        next();
    }
};

module.exports = Movie;

Again, as a review from the last post, a unit test is one that can stand alone and tests specific functionality only on the module/component you are referencing. Any external dependencies should be mocked in some form. Now, for our unit test!

/tests/models/Movie.spec.js
var MovieModel = require('../../api/models/Movie'),
    assert = require('assert'),
    _ = require('underscore'),
    Waterline = require('waterline'),
    SocketIOConstants = require('../../api/constants/SocketIOConstants'),
    diskAdapter = require('sails-disk'); // FYI, sails-memory doesn't seem to play well in a mock situation

var Movie, movie, movieCollection, sails;
var socketEvents = [];


/**
 * Unit tests for the Movie Model.  Intended only to test attribute methods, not Sails or Waterline functionality.
 */
describe('The Movie model', function () {

    /**
     * Create a "sham" model from sails-disk
     */
    before(function(done) {
        // Create a sails stub for socket.io
        sails = {
            io: {
                sockets: {
                    emit: function(event, data) {
                        socketEvents.push(event);
                        return;
                    }
                }
            }
        }
        global.sails = sails;

        // Create a model using sails-disk
        MovieModel.adapter = 'disk';
        Movie = Waterline.Collection.extend(MovieModel);
        new Movie({ adapters: { disk: diskAdapter }}, function(err, collection) {
            if (err) {
                done(err);
            }
            else {
                movieCollection = collection;
                collection.create({ movieId: '1', name: 'The Godfather', releaseDate: new Date(), rating: 'R'})
                    .done(function(err, mockMovie) {
                        if (err) {
                            done(err);
                        }
                        else {
                            movie = mockMovie;
                            done();
                        }
                    });
            }
        });
    });

    describe('attribute methods', function () {
        it('should produce unbiased criticism of the Godfather', function () {
            var criticism = movie.unbiasedCriticism();
            assert(criticism === 'PHENOMENAL!');
        });

        it('should have emitted a movie created event on the socket', function() {
            assert(_.contains(socketEvents, SocketIOConstants.EVENT_MOVIE_CREATED));
        });
    });

    /**
     * Delete everything from sails-disk.  A clean HD is a happy HD.
     */
    after(function(done) {
        movieCollection.destroy().done(function(err) {
            if (err) {
                done(err);
            }
            else {
                done();
            }
        });
    });
});

What The Heck Did We Do?

Our Movie model has two lifecycle callbacks (afterCreate and afterUpdate) and one instance method (unbiasedCriticism).  In order to ensure that the model's methods behave as we expect, we need to spin up a "sham" Waterline.  In this case, we are spinning up Waterline using sails-disk.  We then "create" a sample object on which we can make assertions.  Finally, we need to create a stub sails literal that we can throw in global that has io.sockets.emit defined.  This all happens in the asynchronous "before all" hook of Movie.spec.js.

Once that's all in place, assertions should be elementary.  The unbiasedCriticism method indeed shows that the function brings back 'PHENOMENAL!' and that our stubbed sails.io.sockets fired off the Movie Created event we established in SocketIOConstants.js.  

The "after all" hook simply nukes the object we created on disk so we don't waste any of our disk space.  Easy peasy lemon squeezy.

Other Alternatives

Fernando De Vega created a library called Wolfpack that does the stubbing work for you, like we did above with Waterline.  I haven't messed around with it personally, but it certainly looks promising!  I simply wanted something that I could fully control and understand, hence the approach above.

I hope this helps your unit testing efforts as you continue to Sail away!


4 comments:

Harpua21 said...

Hi Erice!
We are doing this cool thing where we contact amazing people in technology and let them know about forward and fast thinking companies looking to meet them.

Today's special is a firm run by an ex-musician (I use the 'ex' lightly) with a creative outlook and platform that brings national or global brands into the local 'digital' markets they cannot penetrate.

The cool thing about this is that they understand that playing Frankenstein with new technologies in a controlled environment leads to amazing performance breakthroughs and they are looking for people that like doing the same.

As well, the position is located in the Tampa/Saint Pete area which isnt so bad either.

If any of this is interesting enough to give me a ring, please do so on 973-220-9797 and I will fill you in completely!

Best
Matt

Harpua21 said...

We are doing this cool thing where we contact amazing people in technology and let them know about forward and fast thinking companies looking to meet them.

Today's special is a firm run by an ex-musician (I use the 'ex' lightly) with a creative outlook and platform that brings national or global brands into the local 'digital' markets they cannot penetrate.

The cool thing about this is that they understand that playing Frankenstein with new technologies in a controlled environment leads to amazing performance breakthroughs and they are looking for people that like doing the same.

As well, the position is located in the Tampa/Saint Pete area which isnt so bad either.

If any of this is interesting enough to give me a ring, please do so on 973-220-9797 and I will fill you in completely!

Best
Matt

john said...

Hi, Great.. Tutorial is just awesome..It is really helpful for a newbie like me..
I am a regular follower of your blog. Really very informative post you shared here.
Kindly keep blogging. If anyone wants to become a Front end developer learn from Javascript Training in Chennai .
or Javascript Training in Chennai.
Nowadays JavaScript has tons of job opportunities on various vertical industry. ES6 Training in Chennai

Unknown said...

Thank you so much! Had a revelation from looking at this blog.