Testing is essential for any application. I will not dwell on the whys, but instead assume that you are angry with me for skipping this topic in the previous sections. Testing Express applications tend to be relatively straightforward and painless. The general format is that we make fake requests and then make certain assertions about the responses.
We could also implement finer-grained unit tests for more complex logic, but up until now almost everything we did is straightforward enough to be tested on a per route basis. Additionally, testing at the API level provides a more realistic view of how real customers will be interacting with your website and makes tests less brittle in the face of refactoring code.
Mocha is a simple, flexible, test framework runner. First, I would suggest installing Mocha globally so you can easily run tests from the command line as follows:
$ npm install --save-dev –g mocha
The --save-dev
option saves mocha
as a development dependency, meaning we don't have to install Mocha on our production servers. Mocha is just a test runner. We also need an assertion library. There are a variety of solutions, but should.js
syntax, written by the same person as Express and Mocha, gives a clean syntax to make assertions:
$ npm install --save-dev should
The should.js
syntax provides BDD assertions, such as 'hello'.should.equal('hello')
and [1,2].should.have.length(2)
. We can start with a Hello World test example by creating a test
directory with a single file, hello-world.js
, as shown in the following code:
var should = require('should'), describe('The World', function() { it('should say hello', function() { 'Hello, World'.should.equal('Hello, World'), }); it('should say hello asynchronously!', function(done) { setTimeout(function() { 'Hello, World'.should.equal('Hello, World'), done(); }, 300); }); });
We have two different tests both in the same namespace, The World
. The first test is an example of a synchronous test. Mocha executes the function we give to it, sees that no exception gets thrown and the test passes. If, instead, we accept a done
argument in our callback, as we do in the second example, Mocha will intelligently wait until we invoke the callback before checking the validity of our test. For the most part, we will use the second version, in which we must explicitly invoke the done
argument to finish our test because it makes more sense to test Express applications.
Now, if we go back to the command line, we should be able to run Mocha (or node_modules/.bin/mocha
if you didn't install it globally) and see that both of the tests we wrote pass!
Now that we have a basic understanding of how to run tests using Mocha and make assertions with should
syntax, we can apply it to test local user registration. First, we need to introduce another npm
module that will help us test our server programmatically and make assertions about what kind of responses we expect. The library is called supertest
:
$ npm install --save-dev supertest
The library makes testing Express applications a breeze and provides chainable assertions. Let's take a look at an example usage to test our create user route, as shown in the following code:
var should = require('should'), request = require('supertest'), app = require('../server').app, User = require('mongoose').model('User'), describe('Users', function() { before(function(done) { User.remove({}, done); }); describe('registration', function() { it('should register valid user', function(done) { request(app) .post('/users/register') .send({ email: "[email protected]", password: "hello world" }) .expect(302) .end(function(err, res) { res.text.should.containEql("Redirecting to /"); done(err); }); }); }); });
First, notice that we used two namespaces: Users
and registration
. Now, before we run any tests, we remove all users from the database. This is useful to ensure we know where we're starting the tests This will delete all of your saved users though, so it's useful to use a different database in the test environment. Node detects the environment by looking at the NODE_ENV
environment variable. Typically it is test, development, staging, or production. We can do so by changing the database URL in our configuration file to use a different local database when in a test environment and then run Mocha tests with NODE_ENV=test mocha
.
Now, on to the interesting bits! Supertest exposes a chainable API to make requests and assertions about responses. To make a request, we use request(app)
. From there, we specify the HTTP method and path. Then, we can specify a JSON body to send to the server; in this case, an example user registration form. On registration, we expect a redirect, which is a 302
response. If that assertion fails, then the err
argument in our callback will be populated, and the test will fail when we use done(err)
. Additionally, we validate that we were redirected to the route we expect, the server root /
.