Lets take a look at testing node.js using a few popular tools and frameworks for running and writing tests.
The topics I covered previously with TDD will translate well to this article. For a refresher on that, I suggest taking another look at my previous post on TDD with Jasmine and Karma.
On a side note, the code referenced in this post is from my Boilerplate web app using Backbone.js, ExpressJS, Node.js, and MongoDB. Be sure to check out that post so you can get the project setup and running so you can follow along with the rest of this post.
Getting started
[sourcecode light=”true”]
$ npm install -g grunt-cli
$ git clone https://github.com/jkat98/benm.git
$ cd benm
$ npm install
$ grunt init:dev build:dev
[/sourcecode]
We are going to use a few different tools on the server side to handle testing of our node code. Primarily we are using Mocha and a Grunt task to execute our tests. The tests themselves are written using Chai as the assertion language, Sinon for spies and stubs, and Proxyquire to do a little magic when it comes to the idea of “fakes” in the node world.
To run the tests for the code, execute the following command:
[sourcecode light=”true”]
$ grunt test:server
[/sourcecode]
This will execute the mocha runner, and all of the tests we have in the spec folder will be run. You should see the output for each test right in your terminal, hopefully with green checkmarks all the way down the list.
Spechelper.js
The tests are organized in a similar fashion to the app itself, with an app and controllers folder. The main file in the spec folder, spechelper.js, is actually a global helper that just predefines some stuff so that we don’t have to do that in every test file:
[sourcecode lang=”javascript”]
var chai = require(‘chai’),
sinonChai = require(“sinon-chai”);
global.expect = chai.expect;
global.sinon = require(‘sinon’);
chai.use(sinonChai);
[/sourcecode]
In this code, we are declaring that we want in every test: chai, sinonChai, and a few globals defined. Finally we inform chai to use the sinonChai plugin so we can easily do assertions against stubs and spies.
Grunt mocha test run task
Here is the task we have defined in our Gruntfile.js:
[sourcecode lang=”javascript”]
simplemocha: {
options: {
globals: [‘expect’, ‘sinon’],
timeout: 3000,
ignoreLeaks: false,
ui: ‘bdd’,
reporter: ‘spec’
},
server: {
src: [‘spec/spechelper.js’, ‘spec/**/*.test.js’]
}
},
[/sourcecode]
This just configures the grunt-simplemocha task and defines a few options – specifically the globals we setup earlier in our spechelper.js. Then it loads the spechelper.js file and all of our tests.
Writing some basic tests
The tests that we wrote for the models in the app are pretty basic. This is a good file to start with as we break down the code and see whats going on:
[sourcecode lang=”javascript”]
var models = require(‘../../app/models’);
[/sourcecode]
First we ensure that we actually include the module we want to write tests against. Then we include our standard describe statements.
Next what we really want to test, primarily with our models, is that they are properly defined. We dont want to test any actual functionality because they dont actually have any (short of the built in functionality and you dont ever want to write tests for functionality thats assumed to work – i.e. that of a 3rd party vendor module etc).
[sourcecode lang=”javascript”]
it(‘should have email string field’, function() {
expect(schema.email).to.exist;
expect(schema.email.instance).to.equal(‘String’);
});
[/sourcecode]
Pretty simple. We just assert that the schema should have an email field, and that it should be a String. We do this for every field in our model to ensure that the model is always defined, at a minimum, exactly how we expect. Should someone come in later and just delete the email field from the model code, or change it to emailAddress, our tests will fail right away (and thats good!).
Using Proxyquire to stub required modules
With those simple tests out of the way, lets take a look at something a little more complex. The contacts controller relies on a few outside modules so lets see how we would handle writing tests against code that calls other code. The first thing we see in the contacts.test.js file is that theres a lot more going on at the very top:
[sourcecode lang=”javascript”]
var proxyquire = require(‘proxyquire’),
modelsStub = {},
md5Stub = function(string) { return string; },
contacts = proxyquire(‘../../controllers/contacts’, {
‘../app/models’ : modelsStub,
‘MD5’: md5Stub
});
[/sourcecode]
A few interesting things are happening here. First, we are requiring proxyquire, then we create a modelsStub and md5Stub. The modelsStub is just an empty object and the md5Stub is just a super basic function. These stubs is how we are going to use Proxyquire to “Fake” out modules that we require in a file that we want to test. Then, instead of just using a regular require for the contacts controller, we use proxyquire instead, using the same syntax for the path to the contacts controller, but then as a 2nd parameter we provide a collection of stubs that will override modules that are required inside the contacts controller.
**Important Note: One thing to be aware of when using Proxyquire is that the path for each stubbed module is not relative to your test file, but matches exactly as its called in the file thats actually being tested. In this case the “../app/models” line is exactly how it appears in the contacts.js controller file.
By using Proxyquire and stubbing modules that are required inside the file we are testing, we can force the behavior without the actual functionality of those modules effecting our code. A great example of this would be a module that makes a network call. We obviously don’t want network conditions to effect our tests (and ideally we want our tests to be able to run and pass without an active connection to any network at all). To get around this, you can fake out any modules that would typically make a network call with a function of your own that has no logic and simply executes a callback or returns forced values. But this isn’t just for modules that make network calls, its really for modules that do anything (outside of the scope of the file we are testing)!
[sourcecode lang=”javascript”]
it(‘should return json on save’, function() {
modelsStub.Contact = sinon.spy(function() {
modelsStub.Contact.prototype.save = function(callback) {
callback(null, req.body);
};
return;
});
contacts.add(req, res);
expect(res.json).calledWith(req.body);
});
[/sourcecode]
As you can see in the above code, we are executing a test against code that expects the Contact model to be able to save. The Contact model and saving are both dependent on an actual working db server. We don’t care about that in this particular test though, so we are just going to provide a fake save method that only executes a callback (in essence, the conditions that occur in a perfect execution of the real Contact model save function). Not only that, but the fake implementation we provide is actually a stubbed Sinon spy so that we can know when its executed.
The primary responsibility of the contacts controller add function is to execute .save() on a new model and return that model as JSON. So all we care about is that the res.json() is executed and that the data passed into it matches basically what it received (req.body).
Taking this a step further, another assertion we could add (and probably should) is:
[sourcecode lang=”javascript”]
it(‘should call a models save function’, function() {
modelsStub.Contact = sinon.spy(function() {
modelsStub.Contact.prototype.save = sinon.spy();
return;
});
contacts.add(req, res);
expect(modelsStub.Contact.prototype.save).calledOnce;
});
[/sourcecode]
Which basically says we want to be sure that the models .save() function is called. You may be thinking we should have combined both tests and just issued 2 expect statements. But its cleaner to keep the conditions for each test separate. A test shouldn’t ever really read “assert this AND that”.
Using Proxyquire can get confusing, especially with the nature of callbacks with node.js and having to generate stubs on top of stubs, so just be sure to really take your time and properly map out in your head the path that you code takes and understand the flow and how/when/where stubs will be called.