Skellington-Closet / slack-mock

A Slack API mocker for Slack bot integration tests.
MIT License
64 stars 15 forks source link
slack slack-bot testing-tools

slack-mock

Build Status Coverage Status Standard - JavaScript Style Guide

A Slack API mocker for all your Slack bot and Slack app integration tests.

Mock All Slack APIs

Slack Mock will mock all seven ways of pushing data into and pulling data from Slack. You can use it to mock calls to

You can use your API calls as is without changing any URLs or tokens. Slack Mock will capture all outbound HTTP requests to https://slack.com and https://hooks.slack.com, so Slack will never receive your API calls.

With Slack-Mock you can inspect all outbound requests and trigger incoming requests to make sure your bot is doing the right thing.

No Magic Included

OK, there's a little magic included in capturing HTTP requests, but that's it. No timeouts, magic promises, or events. Integration tests are hard, trying to make them easy with "convenience" abstractions that are out of your control only makes them harder.

Integration test by their nature are testing a closed system: you are inspecting from the outside a complex flow between at least two entities (your bot and the Slack API) and there is no guaranteed way to know when that flow is complete by observing from the outside. Any attempt to guess when the communication is complete will be wrong some of the time and just cause you frustration.

That's why Slack Mock provides simple, synchronous methods to queue, trigger, and inspect messages to and from Slack. No magic included.

To write a Slack Mock integration test queue up responses from Slack to your bot, then use Slack Mock to send a message from Slack to your bot to trigger a bot action, wait some time, then assert that your bot made the correct calls to Slack in order. How long do you wait? It depends on what your bot is doing. Play around a little and see what works. I find a 50 millisecond wait is more than enough for most flows.

Usage

See the examples tests for full examples of mocking both a single-team RTM bot and a full Slack App. You can run the examples with npm run examples.

Events API

const payload = {...}

return slackMock.events.send('http://localhost:9000/event', payload)
  .then(delay(50))
  .then(() => {
    expect(slackMock.events.calls).to.have.length(1)
    const firstCall = slackMock.events.calls[0]
    expect(firstCall.statusCode).to.equal(200)
  })

Incoming Webhooks

// incoming webhooks
const firstCall = slackMock.incomingWebhooks.calls[0]
expect(firstCall.params.text).to.equal('hello world')

Interactive Buttons

const payload = {...}

slackMock.interactiveButtons.addResponse({statusCode: 201})

return slackMock.interactiveButtons.send('http://localhost:9000/button', payload)
  .then(delay(75))
  .then(() => {
    expect(slackMock.interactiveButtons.calls).to.have.length(2)
    const responseUrlCall = _.find(slackMock.interactiveButtons.calls, {type: 'response_url'})
    expect(responseUrlCall.params.text).to.equal('GO CUBS')
  })

Outgoing Webhooks

const payload = {...}

return slackMock.outgoingWebhooks.send('http://localhost:9000/outgoing', payload)
  .then(delay(50))
  .then(() => {
    expect(slackMock.outgoingWebhooks.calls).to.have.length(1)
    const firstCall = slackMock.outgoingWebhooks.calls[0]
    expect(firstCall.params.text).to.equal('GO CUBS')
  })

RTM

return slackMock.rtm.send({token: 'abc123', type: 'message', channel: 'mockChannel', user: 'usr', text: 'hello'})
  .then(delay(50))
  .then(() => {
    expect(slackMock.rtm.calls).to.have.length(1)
    expect(slackMock.rtm.calls[0].message.text).to.equal('GO CUBS')
  })

Slash Commands

const payload = {...}

return slackMock.slashCommands.send('http://localhost:9000/slash', payload)
  .then(delay(75))
  .then(() => {
    expect(slackMock.slashCommands.calls).to.have.length(2)

    const responseUrlCall = _.find(slackMock.slashCommands.calls, {type: 'response_url'})
    expect(responseUrlCall.params.text).to.equal('GO CUBS')
    expect(responseUrlCall.params.response_type).to.equal('ephemeral')
  })

OAuth (Web + RTM API)

const botToken = 'xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT'

slackMock.web.addResponse({
  url: 'https://slack.com/api/oauth.access',
  statusCode: 200,
  body: {
    access_token: 'xoxp-XXXXXXXX-XXXXXXXX-XXXXX',
    scope: 'incoming-webhook,commands,bot',
    team_name: 'mockTeam',
    team_id: 'Tmock',
    bot: {
      bot_user_id: 'Bmock',
      bot_access_token: botToken
    }
  }
})

slackMock.web.addResponse({
  url: 'https://slack.com/api/rtm.start',
  statusCode: 200,
  body: {
    ok: true,
    self: {
      name: 'mockSelf',
      id: 'Bmock'
    },
    team: {
      name: 'mockTeam',
      id: 'Tmock'
    }
  }
})

request({
  method: 'POST',
  uri: 'http://localhost:9000/oauth',
  qs: {
    code: 'abc123'
  }
}, (err) => {
  if (err) {
    return console.log(err)
  }

  return delay(250) // wait for oauth flow to complete, rtm to be established
    .then(() => {
      return slackMock.rtm.send(botToken, {type: 'message', channel: 'mockChannel', user: 'usr', text: 'hello'})
    })
    .then(delay(20))
    .then(() => {
      expect(slackMock.rtm.calls).to.have.length(1)
      expect(slackMock.rtm.calls[0].message.text).to.equal('GO CUBS')
    })
    .then(() => done(), (e) => done(e))
})

API Conventions

Slack Mock will intercept all requests to https://slack.com and https://hooks.slack.com. There's no need to change any URLs in your bot.

Here are the method conventions. Not every API wrapper supports each of these methods, see the API docs below:

There is also a top level reset convenience method that will call reset on each API wrapper.

Slack mock will respond to all requests with a 200 OK unless a custom response has been queued. For web requests, a the default body will be {ok: true}.

RTM Conventions

The RTM mocker creates a websocket server to intercept RTM websocket calls. This means unlike the other API mocks there are some truly asynchronous methods in the RTM mocker: send and stopServer. Both of these methods return promises that can be resolved or rejected.

An RTM server will automatically be created when you call https://slack.com/api/rtm.start and will use the token parameter you pass as a unique identifier for this server. You will receive a URL in the response body for that RTM connection.

You can start and stop the RTM server using the same access token. These methods provide a good way to test reconnection attempts by your bot as well as let you bootstrap and clean up after your tests. While you can start an RTM server explicitly in your tests, there is no need to do this if you call the rtm.start API method, as this will create a server for you.

API

require('slack-mock'): function(config)

The exported function used to start the Slack Mock server. Returns an instance of the server.

Slack Mock is a singleton so can only be configured once per process. Subsequent calls to slackMock() will return the same instance.

Config options are:


instance

The configured instance of the Slack Mock slackMock.instance object. This is the same object returned from require('slack-mock')(config).


instance.events (Events API)

The events object mocks sending payloads from the Slack Events API to your Slack App.


instance.incomingWebhooks (Incoming Webhooks)

The incomingWebhooks object mocks receiving payloads from you Slack App to all Incoming Webhooks at https://hooks.slack.com/.


instance.interactiveButtons (Interactive Buttons)

The interactiveButtons object mocks sending and receiving payloads from Slack interactive buttons to your Slack App.


instance.outgoingingWebhooks (Outgoing Webhooks)

The outgoingingWebhooks object mocks sending and receiving payloads from Slack Outgoing Webhooks to your Slack App.


instance.rtm (RTM)

The rtm object mocks sending and receiving payloads from the Slack RTM API.


instance.slashCommands (Slash Commands)

The slashCommands object mocks sending and receiving payloads from a Slack Slash Command to your Slack App.


instance.web (Web API)

The web object receives requests to the Slack Web API and responds with mocked responses.

This mock can be used both for the Web API and the OAuth endpoint (https://slack.com/oauth/authorize). It supports both GET and POST requests to all endpoints.

The https://slack.com/api/rtm.start call requires a token parameter either as a query parameter or in the POST body. This will be used to create an RTM server. See the RTM docs for more information.


instance.reset: function()

Resets all mocks. A convenience method for calling reset on individual API mocks.