TDD Hubot scripts with gulp+mocha

 日本語

I created 3 Hubot scripts and published to npm, however I worried about that there are no unit tests with them.

So I configured them unit tests with gulp and mocha.

package.json

I added the following npm packages to devDependencies:

"devDependencies": {
  "hubot": "^2.7.5",
  "gulp": "^3.7.0",
  "coffee-script": "^1.7.1",
  "gulp-coffee": "^2.0.1",
  "gulp-util": "^2.2.16",
  "hubot-mock-adapter": "^1.0.0",
  "gulp-mocha": "^0.4.1",
  "nock": "^0.34.1",
  "chai": "^1.9.1",
  "gulp-clean": "^0.3.0",
  "gulp-coffeelint": "^0.3.3",
  "gulp-watch": "^0.6.5"
}

Require modules for testing

I use:

In the top of yourscript_spec.coffee:

# Hubot classes
Robot = require("hubot/src/robot")
TextMessage = require("hubot/src/message").TextMessage

# Load assertion methods to this scope
chai = require 'chai'
nock = require 'nock'
{ expect } = chai

The mock Hubot adapter

hubot-mock-adapter is a simple Hubot adapter that just emits events: send, reply, topic, play immediately.

Create Robot instance with mock-adapter.

robot = new Robot null, 'mock-adapter', yes, 'TestHubot'

This means:

  • Loads adapter from npm modules.
  • Uses hubot-mock-adapter.
  • HTTP server is on.
  • Responds to messages with TestHubot ... prefix.

Load your script

To load scripts to test, call Robot:loadFile.

This method loads listeners and parses command helps written in comment.

Loading adapter should be connected to data source. So call them in connected event handler.

robot.adapter.on 'connected', ->
  # Project script
  robot.loadFile path.resolve('.', 'src', 'scripts'), 'browserstack.coffee'
  # Path to scripts bundled in hubot npm module
  hubotScripts = path.resolve 'node_modules', 'hubot', 'src', 'scripts'
  robot.loadFile hubotScripts, 'help.coffee'

hubot help command is implemented in help.coffee.

Test help commands

Robot:loadFile method loads scripts to parse command helps to list in help asynchronously.

So you need to wait for commands to be actually loaded before exit the scope.

do waitForHelp = ->
  if robot.helpCommands().length > 0
    do done
  else
    setTimeout waitForHelp, 100

then you can describe help response.

describe 'help', ->
  it 'should have 3', (done)->
    expect(robot.helpCommands()).to.have.length 3
    do done

  it 'should parse help', (done)->
    adapter.on 'send', (envelope, strings)->
      try
        expect(strings[0]).to.equal """
        TestTestHubot help - Displays all of the help commands that TestHubot knows about.
        TestTestHubot help <query> - Displays all help commands that match <query>.
        TestTestHubot screenshot me <url> - Takes screenshot with Browser Stack.
        """
        do done
      catch e
        done e
    adapter.receive new TextMessage user, 'TestHubot help'

Hubot v2.7.5 has a bug with help parser that adds prefix twice.

I fixed it and sent a pull request#712 that was merged. (not yet published to npm.)

Catch exceptions in event handler

On test failure in event handlers, chai.AssertionError might be thrown in event handlers and that kills process by default.

It needs to try catch and done with error if caught errors.

it 'should handle json parse error', (done)->$
  adapter.on 'send', (envelope, strings)->
    try
      expect(strings[0]).to.equal 'Wont be sent'
      do done
    catch e
      done e
  adapter.receive new TextMessage user, 'TestHubot help'

Mock HTTP

I use nock that overrides native http.ClientRequest module to responses mock data.

nock('http://www.browserstack.com')
  .post('/screenshots')
  .reply 200, job_id: 'abcd1234' # JSON response

nock also avoids all HTTP requests, call nock.disableNetConnect() on beforeEach.

do nock.disableNetConnect
http.get 'http://google.com/'
# this code throw NetConnectNotAllowedError with message
# Nock: Not allow net connect for "google.com:80"

For more details, read nock documentation.

Clean stuff on afterEach

Close HTTP server

Error: listen EADDRINUSE will occur without closing express server.

robot.server.close()

Clean HTTP mocking

If you test error handling, same error will occur other again without calling nock.cleanAll().

nock.cleanAll()

gulpfile.coffee

From version 3.7.0, gulp supports gulpfiles written in CoffeeScript.

gulp watch exits on test failure

gulp-watch exits on test failure by default.

To prevent this, emit end event in error handler like the below:

gulp.task 'watch', ->
  gulp.src(['src/**/*.coffee', 'spec/*.coffee'])
    .pipe watch(files)->
      files
        .pipe(coffee(bare: yes)
          .pipe(mocha reporter: process.env.MOCHA_REPORTER || 'nyan')
          .on('error', -> @emit 'end'))

Make sure mocha pipe should pipe to coffee pipe.

comments powered by Disqus