Skip to content
This repository has been archived by the owner on Jan 23, 2018. It is now read-only.

Commit

Permalink
refactored to use promises in on method
Browse files Browse the repository at this point in the history
  • Loading branch information
zspecza committed Sep 5, 2014
1 parent 4947c8a commit 34a383d
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 34 deletions.
70 changes: 60 additions & 10 deletions lib/index.coffee
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
###========================================
ACTUATOR
========================================###

# load dependencies
path = require 'path'
Robot = require 'hubot/src/robot'
{TextMessage} = require 'hubot/src/message'
callbacks = require 'when/callbacks'

###*
* @class Actuator - A singleton wrapper around a mock Hubot adapter that
* makes writing unit tests for Hubot scripts easier.
###
class Actuator

constructor: ->
@hubot_scripts =

@hubot_scripts = # internal reference to core scripts
path.resolve('.', 'node_modules', 'hubot', 'src', 'scripts')

###*
* actuator.initiate - initializes Hubot with a user, loads the script
* that is to be tested, turns Hubot on
* @api public
* @param {Object} settings - only `script:` required for now
* @param {Function} done - test runner's `done` callback
###
initiate: (settings, done) =>

main_script = detect_path.call(@, settings.script)
Expand All @@ -28,28 +46,60 @@ class Actuator

@robot.run()

###*
* actuator.terminate - basically turns the robot and the webserver off.
* this should go in test runner's `afterEach` hook or equivalent
* @api public
###
terminate: ->
@robot.shutdown()
@robot.server.close()
@robot.server.close() # prevents EADDRINUSE error

on: (message, fn, done) =>
@adapter.on 'send', (envelope, strings) ->
try
fn(strings[0])
done()
catch error
done(error)
@adapter.receive new TextMessage(@user, message)
###*
* actuator.on - listens for commands and returns a promise for
* Hubot's response. The response thenable is an array
* containing all `msg.send` calls in Hubot's handler
* for that command.
* @api public
* @param {String} message - the Hubot command to listen for
* @return {Promise} - a promise for Hubot's response to the command
###
on: (message) =>
# store the message
text = new TextMessage(@user, message)
# queue a listener for the hubot command
setTimeout(@adapter.receive.bind(@adapter, text), 10)
# return a promise for the response
callbacks.call(@adapter.on.bind(@adapter), 'send')
.spread (envelope, responses) ->
return responses

###*
* wait_for_help_to_load - recursively checks if help commands have loaded.
* @api private
* @param {Function} done - done callback from test runner
###
wait_for_help_to_load = (done) ->
# Hubot takes a while to initialize due to the fact that it
# has to parse the comment documentation header at the top of
# each script in order to build a list of help commands.
# This step is necessary to wait for Hubot to do it's thing before
# we continue.
if @robot.helpCommands().length > 0
done()
else
setImmediate(wait_for_help_to_load.bind(@, done))

###*
* detect_path - splits a path into two parts; directory & target file
* @api private
* @param {String} location - the path to split into parts
* @return {Array} - [directory, target file]
###
detect_path = (location) ->
basename = path.basename(location)
location = path.resolve(location.replace(basename, ''))
return [location, basename]

# export singleton as module
module.exports = new Actuator()
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
},
"dependencies": {
"hubot": "^2.8.1",
"hubot-mock-adapter": "^1.0.0"
"hubot-mock-adapter": "^1.0.0",
"when": "^3.4.5"
},
"devDependencies": {
"chai": "^1.9.1",
Expand Down
119 changes: 106 additions & 13 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ Actuator
[![](http://img.shields.io/david/dev/io-digital/actuator.svg?style=flat)](https://david-dm.org/io-digital/actuator#info=devDependencies)
[![](http://img.shields.io/coveralls/io-digital/actuator.svg?style=flat)](https://coveralls.io/r/io-digital/actuator)

Actuator is a tiny wrapper around a mock hubot adapter that makes it easier
to write unit tests for Hubot scripts.

> **Note:** This project is in early development, and versioning is a little different.
[Read this](http://markup.im/#q4_cRZ1Q) for more details.

Installation
------------

```bash
$ npm install actuator --save
$ npm install actuator --save-dev
```

Actuator is a tiny wrapper around a mock hubot adapter for easily writing unit tests for Hubot scripts.

### Usage example:

```coffeescript
Expand All @@ -33,16 +40,102 @@ describe 'test hubot script', ->
done()

it 'should parse help', (done) ->
hubot.on('hubot help', (response) ->
expect(response).to.equal """
hubot actuator - actuator is awesome.
hubot help - Displays all of the help commands that hubot knows about.
hubot help <query> - Displays all help commands that match <query>.
"""
, done)
hubot.on('hubot help')
.spread (response) ->
expect(response).to.equal """
hubot actuator - actuator is awesome.
hubot help - Displays all of the help commands that hubot knows about.
hubot help <query> - Displays all help commands that match <query>.
"""
.done(done.bind(@, null), done)

it 'should respond to messages', (done) ->
hubot.on('hubot actuator', (response) ->
expect(response).to.equal 'actuator is awesome'
, done)
hubot.on('hubot actuator')
.spread (response) ->
expect(response).to.equal 'actuator is awesome'
.done(done.bind(@, null), done)
```

API Usage
---------

### `actuator.initiate(settings, done)`
This starts up a Hubot instance to run all your tests against. It is required
for this module to work, and belongs in your test runner's `beforeEach` hook.
It is asynchronous, and requires `done` to be passed to it from `beforeEach`.
`settings` is a JavaScript object with only one property at the moment: `script`.
`settings.script` is essentially just the path to the script that you want to test.

e.g.

```coffeescript
beforeEach (done) ->
actuator.initiate(script: './lib/your_hubot_script.coffee', done)
```

### `actuator.terminate()`
This shuts down the Hubot instance and it's webserver. Calling this in your
test runner's `afterEach` hook is necessary in order to prevent any weird
errors (like the ports Hubot runs on being regarded as in use).

e.g.

```coffeescript
afterEach ->
actuator.terminate()
```

### `actuator.robot`
This is a direct reference to the Hubot instance itself. Any properties
you might need to reference from Hubot can be found here.

e.g.

```coffeescript
it 'should have 3 help commands', (done) ->
expect(actuator.robot.helpCommands()).to.have.length(3)
done()
```

### `actuator.on(command)`
This is where the magic happens. This method is used to listen for Hubot commands
and assert their response. `command` is a string for the command Hubot should be
listening for.

This method is asynchronous and returns a promise for
Hubot's response to the command. The `responses` thenable is an array of all
the `msg.send` calls in Hubot's handler for that command.

For example, if your Hubot script listens for `"hubot greet me twice"`, like so:

```coffeescript
module.exports = (robot) ->
robot.respond /greet me twice/i, (msg) ->
msg.send("Hi there.")
msg.send("Wassup!?")
```

...then this is what your test would look like:

```coffeescript
it 'responds with two greetings', (done) ->
actuator.on('hubot greet me twice')
.then (responses) ->
expect(responses[0]).to.equal "Hi there."
expect(responses[1]).to.equal "Wassup!?"
.then -> done()
.catch done
```

Since this returns a [when.js promise](https://github.com/cujojs/when)
(which has some [excellent documentation](https://github.com/cujojs/when/blob/master/docs/api.md)),
we can actually make the above test simpler like so:

```coffeescript
it 'responds with two greetings', (done) ->
actuator.on('hubot greet me twice')
.spread (first_greeting, second_greeting) ->
expect(first_greeting).to.equal "Hi there."
expect(second_greeting).to.equal "Wassup!?"
.done(done.bind(@, null), done)
```
22 changes: 12 additions & 10 deletions test/test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ describe 'test hubot script', ->
done()

it 'should parse help', (done) ->
hubot.on('hubot help', (response) ->
expect(response).to.equal """
hubot actuator - actuator is awesome.
hubot help - Displays all of the help commands that hubot knows about.
hubot help <query> - Displays all help commands that match <query>.
"""
, done)
hubot.on('hubot help')
.spread (response) ->
expect(response).to.equal """
hubot actuator - actuator is awesome.
hubot help - Displays all of the help commands that hubot knows about.
hubot help <query> - Displays all help commands that match <query>.
"""
.done(done.bind(@, null), done)

it 'should respond to messages', (done) ->
hubot.on('hubot actuator', (response) ->
expect(response).to.equal 'actuator is awesome'
, done)
hubot.on('hubot actuator')
.spread (response) ->
expect(response).to.equal 'actuator is awesome'
.done(done.bind(@, null), done)

0 comments on commit 34a383d

Please sign in to comment.