Jasmine and NodeJS Testing

As you know if you’ve read my previous posts, I’m still wrapping my head around this whole development thing. Having worked on websites for a long time from the product side, I hear engineers talk about things like BDD, writing tests, etc and how it’s definitely the right way to go. As such, I’m striving to do things the ‘right way™’

After poking around through a few different NodeJS testing frameworks, I settled on Jasmine (mainly because we use it at Bookish and I liked how the specs look when written in Coffeescript). There’s a npm module called jasmine-node that gives you the basic toolset you need to get Jasmine tests working, but it unfortunately doesn’t come with any good documentation on how to setup your test runner or anything like that. Here’s the best way I’ve found to run tests from the command line so you can run them locally or from your CI environment.

Step 1: Install jasmine-node

Note: This requires npm. If you need help installing npm, click here

Installing jasmine-node is actually quite simple. Just use the following command from your project’s base directory:

npm install jasmine-node

This will create a folder under ./node_modules/ called ‘jasmine-node’ that contains the command line version of jasmine which is compatible with nodeJS. If you want to check out the jasmine-node source tree before installing, take a look at github repo.

Step 2: Create your test runner

This is where, in my opinion, the jasmine-node docs fall short. Installing jasmine-node is a cinch, but you have no idea where to go from there. The first thing I figured out is that jasmine-node just gives you the simple executable that will output the results of all of your tests as well as the framework for testing, but it doesn’t give you the actual script to run the damn tests :D

As such, I’ve stolen a really great test runner script for jasmine-node. I can’t remember where I got it from, but it should work for you. I call this file ‘runtests.coffee‘ and place it in my ./tests/ directory:

./tests/runtests.coffee (download)

jasmine = require 'jasmine-node'
sys = require 'sys'

for key in jasmine
  global[key] = jasmine[key]

isVerbose = true
showColors = true
coffee = true

process.argv.forEach (arg) ->
  switch arg
    when '--color' then showColors = true
    when '--noColor' then showColors = false
    when '--verbose' then isVerbose = true
    when '--coffee' then coffee = true

jasmine.executeSpecsInFolder __dirname + '/spec', ((runner, log) ->
  if runner.results().failedCount == 0
    process.exit 0
  else
    process.exit 1
), isVerbose, showColors

As you can see, the testrunner loads jasmine, parses the command line parameters, and then runs each spec file in the ./spec directory.

Once you’ve placed your testrunner at ./spec/runtests.coffee and run it through the coffeescript compiler, you can make sure it works by running the following command from the base directory of your project:

node ./tests/runtests.js

If something’s busted, node will throw an exception and otherwise, you’ll see something like 0 tests, 0 assertions, 0 failures Nice!

Bonus points: To create a simple cake command to run your tests, add it to your Cakefile! (example)

Step 3: Writing your first spec

Most testing frameworks have the concept of a test suite and jasmine is no different. Each file defined under ‘./spec‘ represents a set of tests and within that set of tests, you can create a number of features, a number of tests for each feature, and a number of assertions for each test. If that last sentence sounded like jibberish, check out the jasmine suites and specs documentation. I won’t go into different spec-writing philosophies, as I’m still learning that myself.

To create a spec, you need to make a new file under ./spec Older versions of jasmine-node required you to have ‘spec’ in the filename but that doesn’t seem to be the case any more.

Let’s start with an example from one of my projects, node-towerd, a tower defense game written in node.

In my game, there is a controller called ‘world’ which runs the show. It proxies events to and from game objects as well as to and from the client. Within the world, there are a number of game entities like maps, mobs, and towers. Let’s take a look at the spec for towers:

./tests/spec/towerspec.coffee (download)

### Tower Tests ###
basedir = '../../'
App = require basedir + 'app.js'
Tower = (require basedir + 'controllers/towers.js').Tower
TowerModel = require basedir + 'models/tower-model.js'
Mob = (require basedir + 'controllers/mobs.js').Mob
MobModel = require basedir + 'models/mob-model.js'

Obj = (require basedir + 'controllers/utils/object.js').Obj

# Unit Tests
describe 'Towers towers.js', ->
  beforeEach ->
    world = new Obj # Required because maps relies on 'world' for some events

    # Stub data
    @name = 'Cannon Tower'
    @id = 'cannon'
    @active = 1
    @symbol = 'C'
    @damage = 1
    @range = 2

    @fakeMob = new Obj # Load a fake mob to emit events
    @fakeMob.symbol = 'm'

    @tower = new Tower @id, world

  it 'Loads a new tower called Cannon Tower', ->
    expect(@tower.id).toEqual(@id)
    expect(@tower.name).toEqual(@name)
    expect(@tower.active).toEqual(@active)
    expect(@tower.damage).toEqual(@damage)
    expect(@tower.range).toEqual(@range)

  it 'Saves itself to the DB once loaded', ->
    TowerModel.find { id: @id }, (err, res) =>
      expect(res[0].name).toEqual @name

  it 'Spawns itself on the map at 5, 4', ->
    @tower.on 'spawn', (type, x, y, callback) =>
      expect(@tower.x).toEqual(5)
      expect(@tower.y).toEqual(4)
    @tower.spawn 5, 4, (callback) ->

  it 'Finds no targets when none are in range', ->

    fakeWorld = new Obj

    # Spawn the tower
    @tower.spawn 5, 4, (callback) ->

    # Spawn a fake mob
    fakeMob = new Mob 'warrior', fakeWorld

    fakeMob.spawn 0, 0, (callback) ->

    @tower.checkTarget fakeMob, (res) ->
      expect(res).toEqual []

  it 'Fires on a target when one is in range', ->
    fakeWorld = new Obj

    # Spawn the tower
    @tower.spawn 5, 4, (callback) ->

    # Spawn a fake mob
    fakeMob = new Mob 'warrior', fakeWorld

    fakeMob.spawn 5, 5, (callback) ->

    @tower.checkTarget fakeMob, (res) ->
      expect(res.id).toEqual 'warrior'

This is a relatively large spec, and I’m sure I’m doing a ton of things wrong, but it should give you a good idea of how most specs are structured. You start by loading all of your dependencies via node’s require command. Note: you don’t have to require jasmine-node, the test-runner takes care of that.

Describe your feature

Next, you describe each feature with a describe 'featurename', -> anonymous function. This lets jasmine know that we’re going to start defining a new feature. If any tests under ‘featurename’ fail, jasmine will report back so you know where to start debugging.

Before and After

Once we’ve described our feature, jasmine lets us group any logic that needs to take place repeatedly before each individual test runs via the beforeEach feature. It’s usage is really simple. If you need to create new objects, reset variables, or wipe your database, just create a function called beforeEach or afterEach like so: beforeEach ->

Tests

Finally, let’s write the damn test! Defining each test is as simple as creating a new function that starts with ‘it’ and writing out what you expect the feature to do in plain english. For example:

it 'Loads a new tower called Cannon Tower', ->
  expect(@tower.id).toEqual(@id)
  expect(@tower.name).toEqual(@name)
  expect(@tower.active).toEqual(@active)
  expect(@tower.damage).toEqual(@damage)
  expect(@tower.range).toEqual(@range)

Within each ‘it’ function, jasmine supports a number of ‘matchers’ that allow you to evaluate whether the feature is working as intended or not. In your jasmine-node reports, these matchers are called ‘assertions.’ In the above example, we make sure that a number of variables are equal to what we’re expecting. You can use matchers/assertions to check if variables are defined (or not defined), make sure a variable is set to a certain value, or even check if an exception is thrown. For more info, you should read the full list of jasmine matchers.

Wrapping up

So you’ve installed jasmine-node, created your test runner, created your first spec, and defined your first feature! You can now run your test suite and see if everything passes.

Warning! When writing asynchronous tests, jasmine-node will not automatically wait for your asynchronous functions and callbacks to run. You need to tell it to wait a callback using the asyncSpecDone() and asyncSpecWait() commands. Their usage is as follows:


describe 'Customers', ->
  it 'Should save a customer\'s address', ->
    var customer = new Customer()
    customer.save (err) ->
      expect(err).not.toBeNull()
      expect(customer.name).toEqual('bob')
      asyncSpecDone() # Tell jasmine that our Async function has finished executing

    asyncSpecWait() # Tell jasmine to wait for the above function to finish executing

To see more example specs from my project node-towerd, click here. To read more about writing specs in jasmine, click here.

Coming soon: Continuous Deployment in NodeJS with Jasmine and Jenkins (once I actually get it working!)

  • http://twitter.com/mcmire Elliot Winkler

    jasmine-node does actually come with an executable to run tests — once you `npm install jasmine-node` you can say `jasmine-node –coffee spec` or `jasmine-node –coffee spec/my_spec.coffee`.

    Something I didn’t realize is that with jasmine-node you have to require every file you’re working with in each spec file you write — there’s no jasmine_config.yml like in regular Jasmine where you can put all the files that will get pre-required. I guess that’s just a limitation of node, though.

    • http://www.facebook.com/profile.php?id=13726514 Brad Dickason

      Hm interesting, I’ll have to try using the jasmine-node executable! I definitely need to mess with the –coffee option because I’m finding it less and less enjoyable to debug .js files these days ;)

      I agree, it’s odd to require every file in each spec. A friend of mine showed me a script he wrote called ‘spec_helper’ which ran before each spec. I believe he placed it in the same directory as his specs and jasmine automatically loaded it, but I’m not sure. I’ve seen next to no documentation on this, but perhaps you could stick your requires in there?

  • Dbashford

    Any luck getting things going with Jenkins? Would love to hear how it went and if you had any pointers. I have this task looming in he near future.

  • Keith Rosenberg

    Cool article! Any chance you might be able to direct a curious person to a similar test runner script that isn’t in Coffee? Or if you’re savvy with JS, that you could write up a vanilla JS version?

  • pavelnikolov

    Did you get Jenkins and Jasmine working?

  • pavelnikolov

    Did you get Jenkins and Jasmine working?