Testing - 18F/charlie GitHub Wiki

Charlie developer documentation > Testing

Charlie has pretty extensive unit-ish tests. These tests are built with Jest as a test runner as well as all of its built-in expectations and mocking functionality.

In addition to Jest and everything it has built-in, Charlie has some testing utilities of its own. For starters, axios, the brain, and all of the internal APIs are mocked. To use mocked utilities in your tests, just require them before requiring the module to be tested:

// Imported a mocked brain to use for testing.
const {
  axios,
  brain,
  utils: { slack, tock },
} = require("../test");

The mocks provided here are all Jest mocks, so they have all the APIs and behaviors as any other Jest mock. It is recommended that part of your test include resetting all mocks at the beginning of each test to cut down on the risk of cross-test pollution:

beforeEach(() => {
  jest.resetAllMocks();
});

Bolt app mock

There is also a helper function called getApp() which will create an object that looks like a very simple Bolt app object where the methods of the app are mocked so you can control their behavior or test how they are used

The app mock handles providing a brain mock, mocks for subscribing to Bolt events (action, event, message), and Bolt's logger.

Accessing handlers

Since the bot will subscribe to events on the mock app, it also provides some utility functions for retrieving the listeners. These utility functions are directly on the mock app object.

  • getHandler(Number index) : Function | null

    Get a message handler that the bot registered with the mock app; e.g., with:

    module.exports = (app) => {
      app.message("hello world", () => {
        // handler here...
      });
    };
    

    index defaults to zero, or for bots that register multiple message handlers, you can provide the index to specify which one you want. If there is no handler at the provided index, returns null

  • getActionHandler(Number index) : Function | null

    Same as above, but action handlers. E.g., those registered with app.action().

  • getEventHandler(Number index) : Function | null

    Same again, but event handlers, registered with app.event().

An example usage:

const { getApp } = require("../utils");
const module = require("./awesomebot");

const app = getApp();           // Get the mocked app...
const awesomebot = module(app); // and pass it into the bot for initialization

// Assert that the bot subscribed to messages
expect(app.message).toHaveBeenCalledWith("awesome", expect.any(Function));

// Get the message handler the bot provided when it subscribed
const handler = app.getHandler();

const say = jest.fn();

// Call the handler
handler({ say });

// And make sure it did what we expected
expect(say).toHaveBeenCalledWith("YOU ARE AWESOME!");

Some lessons learned:

  1. To mock a 3rd-party module, first require it into your test, then use jest.mock() on it, and only then require the module under test. If you require the module under test before mocking, it will get the "real" dependency instead of the mocked one.

  2. For bots that rely on time, check out Jest's fake timers. Charlie does not use the "legacy fake timers."

  3. If a bot does any initialization outside of the main export, use the jest.resetModules() utility to reload the module so you can test and control initialization behavior.

    This should probably be considered a warning sign. There is no good reason bots should initialize before their main method is executed, generally. Many of the ones that do so now are legacy