SailsJS is a popular MVC framework for NodeJS. It is widely used due to its ease of use. However, the official Sails documentation on testing is pretty basic and ambiguous, so a lot of people is left to wonder how to properly test a Sails app.
Learn how to functionally test your SailsJS API using Wolfpack and Supertest.
In this first part of the series, we are going to see the basic setup I use to functional test an API with Wolfpack.
I've uploaded to my Github the boilerplate and article samples I'm going to present below. You can checkout the repo here.
A small intro
When I started working with Sails, I found myself in the situation that I wasn't completely sure on how to test a Sails application. In the simplest terms, the documentation said to lift the Sails application and do the testing with my favorite framework.
I didn't like testing against a database. Too much work involved. For GET transactions, I have to make sure that the database has the proper entries to do the tests well. I also had to make sure my teardown was done correctly, otherwise I could get false positives, or my db could grow really big. I wasn't a fan of that idea.
Looking around, I couldn't find any good docs or plugins that could let me avoid the hassle of testing my models against a db, so I decided to write my own. An thus Wolfpack was created.
What is Wolfpack?
Wolfpack is a database driver, like any other db driver (connector as they are really called) for Sails. The major difference is that Wolfpack does not persist the data; everything is stored in variables which disappear once the tests are done.
Wolfpack is also a testing helper. It basically injects itself into a Sails (Waterline) model you provide and spies upon every method in that model. It allows you to mock results that will usually be sent by a database, as well as mock errors that may happen so you can also test error scenarios in realtime.
Overall, Wolfpack lets you mock the results coming from your models, on a case-by-case basis, giving you a fair amount of control on how to run the tests on your app. Let's get to business.
Requirements
For this entire series, I'm assuming you know your way around a Sails project. I'm also assuming you've done test driven development, or at least wrote tests in the past using mocha or jasmine.
On the Sails side, I'm assuming you've disabled Sails default blueprints, which basically create "magic" routes and controllers for our models. If you don't disable blueprints, some of the steps described in this article may not perform as expected.
Setting up the test environment
This first part is all about readying up our environment for proper testing. Let's get to business then.
The following is my boilerplate test environment for a SailsJS application. I use Mocha, Chai and Supertest for my tests. Chai is best suited for Unit testing a Sails application. For functional testing an API, I prefer to use Supertest as it allows me to create a session and easily test HTTP verbs against my API.
From the boilerplate below, you are going to notice I'm setting up stuff I will not be using in this article (coverage and assertion libraries for example), since I'm going to be doing only functional testing. However, you will notice there are some scenarios in which a unit test is better suited. My setup allows me to do both unit and functional testing with ease.
I'll start by assuming you've already setup your Sails project using the Sails client tool and the directory structure is all setup correctly. This is important because my setup uses some tasks already implemented by the Sails team.
First things first, let's make sure we have Grunt installed globally. Type in grunt -v
. If you get an error, then install Grunt globally like this:
npm install -g grunt-cli
It is extremely important to have Grunt installed as the setup I'm using below uses Grunt for almost everything. If you prefer, you can use Gulp as well, but you'll need to tweak some stuff to get my setup working in Gulp.
Also, please note that Sails uses grunt by default, so you may also need to change some tasks there if you prefer to use Gulp or other task runner.
Now that we have Grunt installed, let's install our dev dependencies:
npm install --save-dev blanket chai grunt-blanket grunt-mocha-test mocha sinon supertest wolfpack
There's an additional dependency I install, and that is Lodash. Lodash is a utility library like Underscore, but with a lot more methods and (arguably) better performance. You can use Underscore if you prefer, but just bear in mind you'll need to change the lodash into underscore in the code.
npm install --save lodash
Now here comes the boiler plate stuff. Let's start by modifying some of Sails grunt tasks and create our own tasks as well.
In your root application folder, open the tasks/config/clean.js file, and add a new task called coverage. Right now we won't deal with code coverage, but in a future article we will, and since this is a boilerplate, lets leave it like that. Your clean.js file should look something like this:
// tasks/config/clean.js
module.exports = function(grunt) {
grunt.config.set('clean', {
dev: ['.tmp/public/**'],
build: ['www'],
coverage: {
src: ['coverage/**']
}
});
grunt.loadNpmTasks('grunt-contrib-clean');
};
Next, create a new file in tasks/config/ and call it blanket.js, and paste the following:
// tasks/config/blanket.js
module.exports = function(grunt) {
grunt.config.set('blanket', {
coverage: {
src: ['api/'],
dest: 'coverage/api/'
}
});
grunt.loadNpmTasks('grunt-blanket');
};
Moving on, open tasks/config/copy.js and add a coverage task as well. The file should look like the one below:
// tasks/config/copy.js
module.exports = function(grunt) {
grunt.config.set('copy', {
dev: {
files: [{
expand: true,
cwd: './assets',
src: ['**/*.!(coffee|less)'],
dest: '.tmp/public'
}]
},
build: {
files: [{
expand: true,
cwd: '.tmp/public',
src: ['**/*'],
dest: 'www'
}]
},
coverage: {
src: ['spec/api/**'],
dest: 'coverage/'
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
};
Great! Now let's setup our test runner task. Create a new file called test.js under tasks/config/ and put the following:
// tasks/config/test.js
module.exports = function(grunt) {
grunt.config.set('mochaTest', {
test: {
options: {
reporter: 'spec'
},
src: ['spec/helpers/**/*.js', 'coverage/spec/api/**/*.js']
},
coverage: {
options: {
reporter: 'html-cov',
quiet: true,
captureFile: 'coverage.html'
},
src: ['coverage/spec/api/**/*.js']
}
});
grunt.loadNpmTasks('grunt-mocha-test');
};
Excellent! We are almost done setting up our test runner. Now we need to register a test task with grunt so that we can run the tests. Under the tasks/register/ folder, create a new file called test.js and add the following:
// config/register/test.js
module.exports = function(grunt) {
grunt.registerTask('test', 'Run tests and code coverage report.', ['clean:coverage', 'blanket', 'copy:coverage', 'mochaTest', 'clean:coverage']);
};
After all this, we need to verify our task is setup correctly. To do so, simply type grunt --help
and look for our task at the bottom. If it shows, we are almost set up. If not, check the files we've just added and modified for anything missing (perhaps a pesky semicolon).
Now it is time to set our test helpers which will initiate Sails and provide us with the assertion libraries. But in order to do so, we need to set up the directory structure for our tests.
On a sidenote, when I created Wolfpack, I did it with the intention not to ever have to lift Sails for testing. However, since I'm doing functional testing against endpoints, it is for the best to run the application, that means lifting Sails, so that we can do a proper testing of the API.
The root of your Sails application should have several folders like api, assets, config, etc. In the root of our project, we are going to create a new folder called spec. You can name this folder differently if you prefer, however, if you do so, don't forget to update the paths in the grunt task files we've created and modified before.
Please note that this is how I setup my directory for the tests. You don't need to follow the same structure I do, but for the purpose of this article, please do so.
Within the spec/ folder, I'm going to create three additional folders: api, fixtures, and helpers. spec/api/ is where I put all my tests, spec/fixtures/ is where I put my fixtures for wolfpack to use, and spec/helpers/ is where I put any test helpers I need, including sails test helpers as we will soon see.
Let's start by setting up our Sails helper. In spec/helpers/ create a new file called sails.js and paste the following:
// spec/helpers/sails.js
var Sails = require('sails'),
_ = require('lodash'),
wolfpack = require('wolfpack'),
fs = require('fs'),
sails;
global.wolfpack = wolfpack;
before(function(done) {
// Increase the Mocha timeout so that Sails has enough time to lift.
this.timeout(30000);
Sails.lift({
// configuration for testing purposes
log: {level: 'silent'}
}, function(err, server) {
sails = server;
if (err) return done(err);
// Lookup models for wolpack injection
var files = _.filter(fs.readdirSync(process.cwd() + '/api/models/'), function(file){
return /\.js$/.test(file);
});
// Inject wolfpack into files
_.each(files, function(file){
file = file.replace(/\.js$/, '');
var spied = wolfpack(process.cwd() + '/api/models/' + file);
global[file] = spied;
sails.models[file.toLowerCase()] = spied;
});
// Set hook path so its easier to call in tests
global.server = sails.hooks.http.app;
done(err, sails);
});
});
after(function(done) {
// here you can clear fixtures, etc.
Sails.lower(done);
});
Couple of things to note here. First, as you may notice, one of the first things I do is to set Mocha's timeout to 30 seconds (30000 milliseconds). This is because sometimes Sails takes a lot of time to lift, more than the 5 second default timeout for asynchronous operations set by Mocha. With the 30 second limit, I give ample time for Sails to finish loading before I start running the tests.
No worries, the 30 second limit just applies to the lifting. The rest of the app still has a 5 second timeout.
Once Sails loads, I start injecting wolfpack into all models loaded by the application. This will allow me to set the fixtures for the tests without having to start wolfpack individually in each model.
Im also exposing two global variables: wolfpack
, which contains the wolfpack instance, and a server
, which is basically a shortname for the sails app hook I'm going to be using in our Supertest tests.
Finally, the after hook makes sure we correctly lower Sails once the tests are run, so that we don't leave it hanging occupying the port used for testing.
Next, create a file under spec/helpers/ called libraries.js and paste the following:
// spec/helpers/libraries.js
var chai = require('chai'),
fs = require('fs');
chai.config.includeStack = true;
global.expect = chai.expect;
global.AssertionError = chai.AssertionError;
global.Assertion = chai.Assertion;
global.assert = chai.assert;
global.sinon = require('sinon');
global.request = require('supertest');
global._ = require('lodash');
// Load fixtures
global.fixtures = {};
_.each(fs.readdirSync(process.cwd() + '/spec/fixtures/'), function(file){
global.fixtures[file.replace(/\.js$/, '').toLowerCase()] = require(process.cwd() + '/spec/fixtures/' + file);
});
What the libraries.js file does is it loads Chai's assertion methods and puts them in the global scope so they can easily be accessed in the tests. However, since in this series I'm functional testing and not unit testing, I will be using Supertest assertion's rather than Chai's.
As you can see as well, I'm loading SinonJS (which is a spy/stub library) and put it in the global scope for easy accessibility, as well as Supertest, which I put in the request
global variable, and I also load lodash in the global scope.
Finally, I also load all files within the fixtures folder and load them into the global object fixtures. This way I preload all fixtures into the tests and to call them I simply need to input the name of the fixture file, without the posterior .js, to be able to access them.
We are now done setting up our testing environment.
What's next?
Well, testing of course! In the next part of this series we are going to start testing our API as any client out there will consume it. Let's move on to the next part then.