Skip to content

Commit

Permalink
feat: add example test to hubot --create output (#1710)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyguerra committed Jan 16, 2024
1 parent 2e65d78 commit a842eb6
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 68 deletions.
78 changes: 14 additions & 64 deletions docs/scripting.md
Expand Up @@ -985,74 +985,24 @@ Response middleware callbacks receive 1 parameters, `context` and are Promises/a
# Testing Hubot Scripts
[hubot-test-helper](https://github.com/mtsmfm/hubot-test-helper) is a good framework for unit testing Hubot scripts. (Note that, in order to use hubot-test-helper, you'll need a recent Node.js version with support for Promises.)
I use [Node's Test Runner](https://nodejs.org/dist/latest-v20.x/docs/api/test.html) for writing and running tests for Hubot.
Install the package in your Hubot instance:
[package.json](../package.json)
`% npm install hubot-test-helper --save-dev`
You'll also need to install:
* a JavaScript testing framework such as *Mocha*
* an assertion library such as *chai* or *expect.js*
* Or just use [Node's Test Runner](https://nodejs.org/dist/latest-v20.x/docs/api/test.html)
You may also want to install:
* a mocking library such as *Sinon.js* (if your script performs webservice calls or
other asynchronous actions)
* Or just use Node Test Runner's Mocking facility
[Note: This section is still refering to Coffeescript, but we've update Hubot for Javascript. We'll have to replace this when we get a JavaScript example.]
Here is a sample script that tests the first couple of commands in the [Hubot sample script](https://github.com/hubotio/generator-hubot/blob/master/generators/app/templates/scripts/example.coffee). This script uses *Mocha*, *chai*, *coffeescript*, and of course *hubot-test-helper*:
**test/example-test.coffee**
```coffeescript
Helper = require('hubot-test-helper')
chai = require 'chai'

expect = chai.expect

helper = new Helper('../scripts/example.coffee')

describe 'example script', ->
beforeEach ->
@room = helper.createRoom()

afterEach ->
@room.destroy()
```json
"scripts": {
"test": "node --test",
}
```
it 'doesn\'t need badgers', ->
@room.user.say('alice', 'did someone call for a badger?').then =>
expect(@room.messages).to.eql [
['alice', 'did someone call for a badger?']
['hubot', 'Badgers? BADGERS? WE DON\'T NEED NO STINKIN BADGERS']
]
```sh
npm t
```
it 'won\'t open the pod bay doors', ->
@room.user.say('bob', '@hubot open the pod bay doors').then =>
expect(@room.messages).to.eql [
['bob', '@hubot open the pod bay doors']
['hubot', '@bob I\'m afraid I can\'t let you do that.']
]
Checkout [Xample.mjs](../test/XampleTest.mjs) for an example that tests the [Xample.mjs](../test/scripts/Xample.mjs) script.
it 'will open the dutch doors', ->
@room.user.say('bob', '@hubot open the dutch doors').then =>
expect(@room.messages).to.eql [
['bob', '@hubot open the dutch doors']
['hubot', '@bob Opening dutch doors']
]
```
In order to isolate your script from Hubot, I've created a [Dummy Adapter](../test/doubles/DummyAdapter.mjs) that you can use when starting a Robot instance to interact with and excercise your code. For now, my suggestion is to copy the `DummyAdapter` into your code so that you can modifiy as your needs evolve.
**sample output**
Please feel free to create Github issues if you have questions or comments. I'm happy to collaborate.
```sh
% mocha --require coffeescript/register test/*.coffee
example script
✓ doesn't need badgers
✓ won't open the pod bay doors
✓ will open the dutch doors
3 passing (212ms)
```
If you created your bot with `npx hubot --create xample-bot`, then the `DummyAdapter` is already there. Along with an example test.
111 changes: 107 additions & 4 deletions src/GenHubot.mjs
Expand Up @@ -26,7 +26,7 @@ function runCommands (hubotDirectory, options) {
}
output = spawnSync('npm', ['i', 'hubot-help@latest', 'hubot-rules@latest', 'hubot-diagnostics@latest'].concat([options.adapter]).filter(Boolean))
console.log('npm i', output.stderr.toString(), output.stdout.toString())
spawnSync('mkdir', ['scripts'])
spawnSync('mkdir', ['scripts', 'tests', 'tests/doubles'])
spawnSync('touch', ['external-scripts.json'])

const externalScriptsPath = path.resolve('./', 'external-scripts.json')
Expand All @@ -39,7 +39,7 @@ function runCommands (hubotDirectory, options) {

File.writeFileSync(externalScriptsPath, JSON.stringify(externalScripts, null, 2))

File.writeFileSync('./scripts/example.mjs', `// Description:
File.writeFileSync('./scripts/Xample.mjs', `// Description:
// Test script
//
// Commands:
Expand All @@ -50,16 +50,119 @@ function runCommands (hubotDirectory, options) {
//
export default (robot) => {
robot.respond(/helo/, async res => {
robot.respond(/helo$/, async res => {
await res.reply("HELO World! I'm Dumbotheelephant.")
})
robot.respond(/helo room/, async res => {
await res.send('Hello World!')
})
}`)

File.writeFileSync('./tests/doubles/DummyAdapter.mjs', `
'use strict'
import { Adapter, TextMessage } from 'hubot'
export class DummyAdapter extends Adapter {
constructor (robot) {
super(robot)
this.name = 'DummyAdapter'
this.messages = new Set()
}
async send (envelope, ...strings) {
this.emit('send', envelope, ...strings)
this.robot.emit('send', envelope, ...strings)
}
async reply (envelope, ...strings) {
this.emit('reply', envelope, ...strings)
this.robot.emit('reply', envelope, ...strings)
}
async topic (envelope, ...strings) {
this.emit('topic', envelope, ...strings)
this.robot.emit('topic', envelope, ...strings)
}
async play (envelope, ...strings) {
this.emit('play', envelope, ...strings)
this.robot.emit('play', envelope, ...strings)
}
run () {
// This is required to get the scripts loaded
this.emit('connected')
}
close () {
this.emit('closed')
}
async say (user, message, room) {
this.messages.add(message)
user.room = room
await this.robot.receive(new TextMessage(user, message))
}
}
export default {
use (robot) {
return new DummyAdapter(robot)
}
}
`)
File.writeFileSync('./tests/XampleTest.mjs', `
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import { Robot } from 'hubot'
// You need a dummy adapter to test scripts
import dummyRobot from './doubles/DummyAdapter.mjs'
// Mocks Aren't Stubs
// https://www.martinfowler.com/articles/mocksArentStubs.html
describe('Xample testing Hubot scripts', () => {
let robot = null
beforeEach(async () => {
robot = new Robot(dummyRobot, false, 'Dumbotheelephant')
await robot.loadAdapter()
await robot.loadFile('./scripts', 'Xample.mjs')
await robot.run()
})
afterEach(() => {
robot.shutdown()
})
it('should reply with expected message', async () => {
const expected = "HELO World! I'm Dumbotheelephant."
const user = robot.brain.userForId('test-user', { name: 'test user' })
let actual = ''
robot.on('reply', (envelope, ...strings) => {
actual = strings.join('')
})
await robot.adapter.say(user, '@Dumbotheelephant helo', 'test-room')
assert.strictEqual(actual, expected)
})
it('should send message to the #general room', async () => {
const expected = 'general'
const user = robot.brain.userForId('test-user', { name: 'test user' })
let actual = ''
robot.on('send', (envelope, ...strings) => {
actual = envelope.room
})
await robot.adapter.say(user, '@Dumbotheelephant helo room', 'general')
assert.strictEqual(actual, expected)
})
})
`)

const packageJsonPath = path.resolve(process.cwd(), 'package.json')
const packageJson = JSON.parse(File.readFileSync(packageJsonPath, 'utf8'))

packageJson.scripts = {
start: 'hubot'
start: 'hubot',
test: 'node --test'
}
packageJson.description = 'A simple helpful robot for your Company'
if (options.adapter) {
Expand Down
45 changes: 45 additions & 0 deletions test/XampleTest.mjs
@@ -0,0 +1,45 @@
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'

// Replace this with import { Robot } from 'hubot'
import { Robot } from '../index.mjs'

// You need a dummy adapter to test scripts
import dummyRobot from './doubles/DummyAdapter.mjs'

// Mocks Aren't Stubs
// https://www.martinfowler.com/articles/mocksArentStubs.html

describe('Xample testing Hubot scripts', () => {
let robot = null
beforeEach(async () => {
robot = new Robot(dummyRobot, false, 'Dumbotheelephant')
await robot.loadAdapter()
await robot.loadFile('./test/scripts', 'Xample.mjs')
await robot.run()
})
afterEach(() => {
robot.shutdown()
})
it('should reply with expected message', async () => {
const expected = 'HELO World! I\'m Dumbotheelephant.'
const user = robot.brain.userForId('test-user', { name: 'test user' })
let actual = ''
robot.on('reply', (envelope, ...strings) => {
actual = strings.join('')
})
await robot.adapter.say(user, '@Dumbotheelephant helo', 'test-room')
assert.strictEqual(actual, expected)
})

it('should send message to the #general room', async () => {
const expected = 'general'
const user = robot.brain.userForId('test-user', { name: 'test user' })
let actual = ''
robot.on('send', (envelope, ...strings) => {
actual = envelope.room
})
await robot.adapter.say(user, '@Dumbotheelephant helo room', 'general')
assert.strictEqual(actual, expected)
})
})
51 changes: 51 additions & 0 deletions test/doubles/DummyAdapter.mjs
@@ -0,0 +1,51 @@
'use strict'
// Replace this with import { Adapter, TextMessage } from 'hubot'
import { Adapter, TextMessage } from '../../index.mjs'

export class DummyAdapter extends Adapter {
constructor (robot) {
super(robot)
this.name = 'DummyAdapter'
this.messages = new Set()
}

async send (envelope, ...strings) {
this.emit('send', envelope, ...strings)
this.robot.emit('send', envelope, ...strings)
}

async reply (envelope, ...strings) {
this.emit('reply', envelope, ...strings)
this.robot.emit('reply', envelope, ...strings)
}

async topic (envelope, ...strings) {
this.emit('topic', envelope, ...strings)
this.robot.emit('topic', envelope, ...strings)
}

async play (envelope, ...strings) {
this.emit('play', envelope, ...strings)
this.robot.emit('play', envelope, ...strings)
}

run () {
// This is required to get the scripts loaded
this.emit('connected')
}

close () {
this.emit('closed')
}

async say (user, message, room) {
this.messages.add(message)
user.room = room
await this.robot.receive(new TextMessage(user, message))
}
}
export default {
use (robot) {
return new DummyAdapter(robot)
}
}
18 changes: 18 additions & 0 deletions test/scripts/Xample.mjs
@@ -0,0 +1,18 @@
// Description:
// Test script
//
// Commands:
// hubot helo - Responds with HELO World!.
//
// Notes:
// This is a test script.
//

export default (robot) => {
robot.respond(/helo$/, async res => {
await res.reply(`HELO World! I'm ${robot.name}.`)
})
robot.respond(/helo (.*)/gi, async res => {
await res.send(`Hello World! I'm ${robot.name}.`)
})
}

0 comments on commit a842eb6

Please sign in to comment.