From a842eb6a1a22dc8d8c3f60fe93b8146565e95565 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Mon, 15 Jan 2024 22:37:18 -0600 Subject: [PATCH] feat: add example test to hubot --create output (#1710) --- docs/scripting.md | 78 +++++------------------- src/GenHubot.mjs | 111 ++++++++++++++++++++++++++++++++-- test/XampleTest.mjs | 45 ++++++++++++++ test/doubles/DummyAdapter.mjs | 51 ++++++++++++++++ test/scripts/Xample.mjs | 18 ++++++ 5 files changed, 235 insertions(+), 68 deletions(-) create mode 100644 test/XampleTest.mjs create mode 100644 test/doubles/DummyAdapter.mjs create mode 100644 test/scripts/Xample.mjs diff --git a/docs/scripting.md b/docs/scripting.md index c9a7ed3de..ffbaafc81 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -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. diff --git a/src/GenHubot.mjs b/src/GenHubot.mjs index 048d90c01..623cd96c1 100755 --- a/src/GenHubot.mjs +++ b/src/GenHubot.mjs @@ -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') @@ -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: @@ -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) { diff --git a/test/XampleTest.mjs b/test/XampleTest.mjs new file mode 100644 index 000000000..2c3705b7d --- /dev/null +++ b/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) + }) +}) diff --git a/test/doubles/DummyAdapter.mjs b/test/doubles/DummyAdapter.mjs new file mode 100644 index 000000000..6bf5b93a3 --- /dev/null +++ b/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) + } +} diff --git a/test/scripts/Xample.mjs b/test/scripts/Xample.mjs new file mode 100644 index 000000000..2b4cd5315 --- /dev/null +++ b/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}.`) + }) +}