Testing promises in Node.js with Mocha, Chai and Sinon

Date:
Tags:
Javascript Promises Testing Development
By

To test my Rails projects I typically use rspec. I really enjoy the way it helps me layout and describe my tests.

So when I started writing my first node.js package (back-on-promise), I wanted a similar way in which to write my tests when testing promises. I decided to use mocha for running the tests, chai for test assertions, and sinon to mock and stub objects. In this post I will describe how to test with these tools in node.js, specifically looking at promises.

Note: I use CoffeeScript in this post because I like the syntax and my fingers get sore typing function hundreds of times. If you are unfamiliar with CoffeeScript maybe The Little Book on CoffeeScript can help.

#Testing in node.js Since it is typical for node packages to be very granular, and testing packages are no exception, a node testing framework will consist of many complementary packages. This is different to other frameworks like rails, where the testing framework rspec is a single package that provides assertions, stubbing and a test runner. These aspects of testing have been broken up into individual node packages, where:

  1. Mocha is a testing framework for describing and running tests
  2. Chai is an assertion library
  3. Sinon is a mocking and stubbing library

Note: the granular packages for nodes testing frameworks allows replacement of parts based on preference or suitability, e.g. you could replace chai with should.js.

#The Setup Note: node and coffee and npm are requirements of this project

First, let's create a project called cachy with a package.json: { "name" : "cachy", "description" : "Lets test some promises", "url" : "https://github.com/grahamjenson/test_promises.git", "author" : "Graham Jenson <grahamjenson@maori.geek.nz>", "dependencies" : { "q": "1.0.0", "q-io": "1.10.9" }, "devDependencies": { "mocha": "1.17.1 ", "chai": "1.9.0", "sinon": "1.8.2", "coffee-script": "1.7.1" }, "scripts": { "test": "mocha --compilers coffee:coffee-script/register" } }

Then do the standard npm install to install all the node packages.

The first thing to note about the cachy package is that it uses Q promises with the Q-IO library for the http IO. This is a Promises/A+ spec promises library so its promises can manage exceptions, unlike JQuery promises.

Note: I have previously posted a description of JQuery promises here.

The second part to note is the definition of the test script mocha --compilers coffee:coffee-script/register. The only argument for this is to tell mocha to compile tests with the extension coffee with coffee-script/register.

Note: if you are using coffee-script 1.6 then the argument is just coffee:coffee-script

#The Testy To demonstrate testing promises I am going to implement an http cache for remote JSON. For this I need a get function that takes a url and returns a promise for the data. The benefit of such a method is that it uses the same interface regardless of whether the data has been cached or not. I call the object Cachy and put it in a file called cachy.coffee:

```coffeescript qhttp = require("q-io/http") q = require('q')

Cachy = { _cache : {}

writecache: (key, data) -> @cache[key] = data

readcache: (key) -> @cache[key]

resetcache: -> @cache = {}

get: (url) -> if @cache[url]
return q.fcall(=> @
cache[url]) return qhttp.read(url).then( (buf) => json = JSON.parse(buf) @write_cache(url,json); return json ) }

if (typeof module != 'undefined' && module.exports) module.exports = Cachy; ```

First it imports q and q-io/http. q-io is a package that offers a tidy wrapper around http IO so that calls return a promise.

Second, I define Cachy, this object has a few helper methods to manage the cache (write_cache, read_cache, reset_cache) and the core method get.

get returns a promise for the data at a url. It first looks in the cache to see if the url has already been called; if it is there it returns a promise (created using q.fcall) for the cached object. If the call has not been cached yet it will get a return a promise that takes a qhttp.read (which is a http get call), then parses the returned object to JSON and writes the object to cache.

The final part of this file is just defining what to export when required.

#The Test Now Cachy is ready to be tested! Mocha will look for tests with the .coffee extension in a folder called test. So I created a file test/tests.coffee.

tests.coffee will start similar to cachy.coffee with an import of all the required modules. The only non-standard line is chai.should(), which is called to inject the should methods on objects.

```coffeescript chai = require 'chai' should = chai.should()

sinon = require 'sinon'

q = require 'q' qhttp = require("q-io/http")

Cachy = require '../cachy' ```

Mocha lets you group tests together using the describe method. describe takes a description of the tests and a function defining all the tests.

coffeescript describe 'Cachy.get', ->

In this describe function there are two tests for Cachy.get:

  1. a test for when the data is already cached
  2. a test for when the data must be fetched using qhttp.read

###Already Cached Test

coffeescript describe 'if the data is cached', -> it 'returns a promise for the data from cache', (done) -> url = 'http://www.maori.geek.nz' data = {name: 'maori.geek'} Cachy.write_cache(url, data) Cachy.get(url).then( (data) -> data.name.should.equal 'maori.geek' done() ) .catch((error) -> done(error) ) .fin( -> Cachy.reset_cache() )

Mocha defines its tests using the it function, which takes a description of the test and a function whose first parameter is a done callback. This done callback is used for asynchronous tests; a test will wait some time (default 2 seconds) for the done() callback at which point the test is finished. However, if done(error) is called it will immediately fail the test.

The first test defines a url and some data, then caches it using Cachy.write_cache. Calling Cachy.get(url) should return a promise for the data. If the promise is satisfied then will be called to assert (using Chai's should method) that the data is correct. If the promise fails or errors it will be catch will be called to execute done(error), which fails the test by passing the error to mocha.

Note: As the Promises/A+ spec defines a promise to internally handle errors, the catch method is necessary or the promise will silently fail and the test will incorrectly pass

###Not Yet Cached Test

```coffeescript describe 'if the data is not cached', -> it 'should fetch data, cache and return it', (done) -> url = 'http://www.maori.geek.nz' data = {name: 'maori.geek'}

sinon.stub(qhttp, 'read', (curl) -> 
  curl.should.equal url
  return q.fcall(-> JSON.stringify(data))
)
Cachy.get(url).then( (data) ->
  data.name.should.equal 'maori.geek'
  Cachy.read_cache(url).should.equal data
  done()
)
.catch( (error) ->
  done(error)
)
.fin( (value) ->
  Cachy.reset_cache()
  qhttp.read.restore()
)

```

This test starts off similar to the previous test defining url and data. Then it uses sinon to stub the call to the http server qhttp.read to instead return a promise (created using q.fcall) for a stringified JSON object.

Calling Cachy.get(url) should call the stub to get the data with the url provided, which will then return the data. Once returned the data is asserted to be correct and that it has been cached. If an error occurs, it is caught by catch and the test will fail.

Finally, the test is cleaned up in fin by resetting the cache, and removing the qhttp.read stub with the method restore.

###Running the tests By calling npm test in the console you will get an output similar to this:

> cachy@ test /home/graham/test_promises > mocha --compilers coffee:coffee-script/register -C ․․ 2 passing (15ms)

#Conclusion I like testing, the more I test the more I see its benefits. However, promises and node.js have some idiosyncrasies, due to their asynchronous nature, that must be understood to test. Although, this was not a complete guide to testing in node, I hope it will help you get started.

Note: the code in this post is available on github here

#Some more places for information Node.js in Action

Node.js the Right Way

Derick Bailey: Asynchronous Unit Tests With Mocha, Promises, And WinJS

Not Yet Released: JavaScript with Promises

O'Reilly Learning jQuery Deferreds: Taming Callback Hell with Deferreds and Promises

Related Posts

comments powered by Disqus