Search code examples
testingloggingcoffeescriptmocha.jshubot

How to check Hubot log output in test script


I'm writing a test for my Hubot (which acts as a Slack bot). Triggered by certain Slack messages, the bot sends an HTTP POST request to a separate Rails app. How can I check in my test script that the HTTP request has been sent? My guess was that I should check the contents of the robot.logger (please let me know if there's a better way) - but if so, how can I access the log in the test?

Hubot script (basically, it informs the Rails app about a user who is leaving the office to take a break):

module.exports = (robot) ->
  robot.respond /off to lunch/i, (res) ->
    res.reply('Later alligator')
    robot.logger.info "This user is going on lunch break: #{res.message.user.id}"

    data = JSON.stringify({
      slack_user_id: res.message.user.id
    })
    robot.http(process.env.RAILS_APP_URL + '/break')
      .header('Content-Type', 'application/json')
      .post(data) (err, resp, body) ->
        if err
          robot.logger.info "Encountered an error. #{err}"
          res.reply('Sorry, there was an error recording your break time')
        else
          robot.logger.info 'Successfully sent HTTP POST request to Rails app'

Log output when I execute this script:

INFO This user is going on lunch break: [SLACK_USER_ID]
INFO Successfully sent HTTP POST request to Rails app

As I mentioned above, I'd like to check in my test script that the HTTP request was sent, by asserting that the log is going to include the message 'Successfully sent HTTP POST request to Rails app'. However, I don't know how to access the Hubot's log in my test. I thought it would have something to do with process.stdout because the bot logs to stdout, but I couldn't get it to work.

Test script:

Helper = require('hubot-test-helper')
helper = new Helper('../scripts/break-start.coffee')
request = require('request')
expect = require('chai').expect
nock = require('nock')

describe 'bot responds to user message and sends ', ->
  beforeEach ->
    # Set up the room before running the test.
    @room = helper.createRoom()

    # Set up a request interceptor.
    nock(process.env.RAILS_APP_URL)
      .post('/break', { slack_user_id: 'bob' })
      .reply(200)

  afterEach ->
    # Tear down the room after the test to free up the listener.
    @room.destroy()

  context 'user sends lunch message', ->
    beforeEach ->
      @room.user.say('bob', '@hubot Off to lunch')

    it 'responds to users who are off to lunch', ->
      expect(@room.messages).to.eql [
        ['bob', '@hubot Off to lunch']
        ['hubot', '@bob Later alligator']
      # I want to do something like this:
      # expect(robot.log).include('Successfully sent HTTP POST request to Rails app')

Of course, I can see in the console log when I run the test that the HTTP request is being sent, but I'd also like to assert it so that the test fails if the request is not sent.

Log output when test is executed:

INFO This user is going on lunch break: bob
✓ responds to users who are off to lunch
INFO Successfully sent HTTP POST request to Rails app

Thank you for any help in advance.


Solution

  • I wouldn't advise writing tests depending on logs. The log is a side-effect of the program. If you change the log output, the tests will fail, even though the functionality is still correct.

    Instead you should use a library to mock out and check if the http request was performed. Actually making the request would be a side-effect, and again shouldn't be done in your tests (what if you cause excessive load on an external service due to tests running?

    You are already using the nock library to catch the request. It can also be used to check if the request was made (see the expectations docs from the nock repo).

    Here is an example using the requestScope.done() from nock in your test.

    it 'sends the break request to the rails server', ->
      # capture the request to the rails app
      railsRequest = nock(process.env.RAILS_APP_URL)
        .post('/break', { slack_user_id: 'bob' })
        .reply(200)
      # make the request and wait for it to be completed
      await @room.user.say('bob', '@hubot Off to lunch')
      # check that the request was made
      railsRequest.done()
    

    I'm using await to ensure the function call which should make the request is completed before testing for it. If you're not familiar with await, you can move the check (railsRequest.done()) into a .then() promise handler on the @room.user.say(...) call instead.


    Promise version:

    Regarding your comment, here is the promisified version. You need to pass .then a function. If you pass it .then request.done() then the request.done() expectation will be executed immediately and its result will be passed as the promise callback. Either wrap it in another function (see below) or remove the parenthesis (.then request.done). But be careful with the second option. If your promise returns a value, this will be passed to the callback. As it's from a library, this may cause unexpected behaviour - that's why I would suggest the first option:

    it 'sends the break request to the rails server', ->
      # capture the request to the rails app
      railsRequest = nock(process.env.RAILS_APP_URL)
        .post('/break', { slack_user_id: 'bob' })
        .reply(200)
      # make the request and wait for it to be completed
      @room.user.say('bob', '@hubot Off to lunch').then ->
        # check that the request was made
        railsRequest.done()