Testing in Node.js with Mocha

來自:http://brianstoner.com/blog/testing-in-nodejs-with-mocha/

BRIAN STONER


I recently migrated all of our server side Node tests at Instinct from Vows to Mocha.

I've been around the block with testing in Node, unable to find an approach that I really liked, until I started using Mocha. The elegant way it does async along with familiar BDD style syntax is really enjoyable to work with. It's made writing/maintaining our tests something I don't run from anymore.

Two main reasons I made the switch:

  • Syntax: The bulk of our code for Instinct is on the client-side where we were usingJasmine. Personally, I prefer the BDD style of Jasmine with nested closures over Vows' syntax. I knew getting all our Javascript tests consistently using the same syntax would be a big win and keep me dedicated to writing and maintaining our tests.

  • Better Async Testing on the Client + Server: The way Mocha does async testing is very elegant. I was becoming frustrated with Jasmine's approach to async testing, which is basically to force it to run synchronously with timers. I wanted to first try Mocha on the server, and then if it went well I knew I could somewhat easily migrate my client-side tests to Mocha as well.

Plus we had to do a big re-factor/rewrite of several pieces on the server anyway, so it seemed like a good time to make the switch.

First Steps

First you need to install Mocha. I justed added it to our package.json and installed it locally under our project. Or you can just install it manually:

npm install mocha

We were using Vows for both unit and integration tests. So after installing things, I wanted to first get our unit tests working. This was literally just a matter of updating the syntax of our tests.

From something like this in Vows:

vows.describe('User Model').addBatch({

  'Create a new user' : {

    topic: function(){
      User.create({ username: 'test' }, this.callback);
    },

    'should have a username': function(doc){
      assert.equal( 'test', doc.username );
    }
  }
}).export(module);

To this in Mocha:

  describe('Creating a new User',function(){
    var user;

    before(function(done){
      User.create({ username: 'test' , function(err,u){
        user = u;
        done();        
      });
    });

    it('should have a username',function(){
      user.should.have.property('username','test');
    });
  });

I opted to use the 'should' syntax, which is a separate module you need to install & require in your tests.

Running the tests

To run the tests, I followed TJ's example of setting up a MakeFile in the root of the project, like this:

REPORTER = dot

test:
  @NODE_ENV=test ./node_modules/.bin/mocha \
    --reporter $(REPORTER) \

test-w:
  @NODE_ENV=test ./node_modules/.bin/mocha \
    --reporter $(REPORTER) \
    --growl \
    --watch

.PHONY: test test-w

Then you can just run your tests with:

$ make test

By default mocha will run everything in /tests off of your main project. I've been following the same organization for my test files as TJ uses in Express, where all the tests are in that main /tests directory and the filenames indicate the namespacing.

I opted for the 'dot' reporting output, but there's tons of options. And it's even pretty easy to fork and create your own if you're looking for something specific in your test output.

Auto-running the tests

Just like autotest in Rails, you can setup Mocha to run automatically whenever one of your files changes. That was the purpose of the test-w target in the MakeFile above. So now I could just make my tests run continuously in the background by running:

make test-w

To make it work on Lion with growl notifications, I needed to install growlnotify and thennode-growl.

Setting up 'npm test'

The next thing I did was modify our package.json file so that it runs our tests whenever someone executes 'npm test' in the project.

Everyone in the Node community seems to have their own methods for running tests, but one thing that does seem like it's standardizing is that the command 'npm test' will run the tests for a node project. You can do this just by adding it under the 'scripts' attribute in your package.json file:

{
  "name": "instinct",
  "version": "0.0.1",
  "dependencies": {},
  "scripts": {
    "test": "make test"
  }
}

Now you can also run your tests via npm:

npm test

Migrating our Integration Tests

My next step was getting the integration tests running in Mocha. Our node server is essentially just a json REST API, so our integration tests are largely HTTP requests that test response codes + the json of the response body.

Vows didn't have any built-in support for HTTP testing, so I created my own helper methods to do this. I was a little worried I'd have to rewrite these for Mocha as it doesn't seem to have anything built-in to help with Integration tests.

After poking through the tests for Express however, I found that TJ had a support lib that he was using to test HTTP requests/responses. I was able to create a modified versionthat gave me everything I had in Vows.

And then I was able to write nice looking HTTP tests like this:

var app = require('../app')
  , http = require('support/http');

describe('User API',function(){

  before(function(done){
    http.createServer(app,done);
  });

  it('GET /users should return 200',function(done){
    request()
      .get('/users')
      .expect(200,done);
  });

  it('POST /users should return 200',function(done){
    request()
      .post('/users')
      .set('Content-Type','application/json')
      .write(JSON.stringify({ username: 'test', password: 'pass' }))
      .expect(200,done);
  });
});

'app' is your main Express app and 'http' is the support lib.

Generating a Coverage Report

The last piece of the puzzle was getting a test coverage report. This wasn't something I had setup with Vows, but now seemed like a good time to do it as Mocha's coverage reports looked pretty sweet.

The first thing I did was install node-jscoverage.

To generate a coverage report you need to first instrument your code so that when it runs it can track what lines have been called and which haven't. This is what node-jscoverage does, it will essentially create a copy of every file you tell it to. You then need to make your tests use the instrumented code instead of the normal code.

This is when I realized I had a problem. My source code was primarily in 3 different directories off the project root, 'controllers', 'models' and 'middleware'. I would need to instrument all 3 directories, and then get my tests to be smart about whether to use the instrumented vs. non-instrumented code.

I didn't see a very clean way to do this, so I started poking around how Express and other projects handled this. I found that it becomes very simple if you organize all of your source code under a single directory (i.e. lib) and export it all as a single module. Then you can create an index.js file (just like Express) at the root of the project that simply exports either /lib or /lib-cov based on the some environment variable.

Our project was laid out like this:

config/
controllers/
middleware/
models/
public/
routes/
test/
views/
app.js
package.json
MakeFile

After rearranging things, it now looks somewhat like this:

public/
lib/
  config/
  controllers/
  middleware/
  models/
  instinct.js
routes/
test/
views/
app.js
packageon.json
MakeFile
index.js

I had to add instinct.js that consolidated all my models, controllers, middleware and exported them from one place. Then elsewhere in my project anytime I needed a model, controller or middleware I just required that main instinct module like so:

var instinct = require('./instinct');

Then I added an index.js file at the project root that exports either lib/instinct or lib-cov/instinct depending on the INSTINCT_COV environment variable (which we'll set in our MakeFile):

module.exports = process.env.INSTINCT_COV
  ? require('./lib-cov/instinct')
  : require('./lib/instinct')

Now when node-jscoverage instruments my lib/ code to lib-cov/, if my environment variable INSTINCT_COV is set to true, any require to './instinct' will load the instrumented code.

Now with my project organized, I just had to modify my MakeFile, again borrowing from the MakeFile in Express:

REPORTER = dot

test:
  @NODE_ENV=test ./node_modules/.bin/mocha \
    --reporter $(REPORTER) \

test-w:
  @NODE_ENV=test ./node_modules/.bin/mocha \
    --reporter $(REPORTER) \
    --growl \
    --watch

test-cov: lib-cov
  @INSTINCT_COV=1 $(MAKE) test REPORTER=html-cov > public/coverage.html

lib-cov:
  @jscoverage lib lib-cov

.PHONY: test test-w

And that was it, I was able to generate a coverage report by running:

make test-cov

Next Steps

After running with Mocha on the server for a bit, I was really impressed with how nice it made testing, and confirmed that I definitely wanted to migrate our client-side tests too (which I have since completed & plan to write about in another post).


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章