Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding unit tests examples to the documentation #383

Closed
4 of 9 tasks
szymon-szym opened this issue Jan 25, 2020 · 10 comments
Closed
4 of 9 tasks

Adding unit tests examples to the documentation #383

szymon-szym opened this issue Jan 25, 2020 · 10 comments
Labels
auto-triage-skip Prevent this issue from being closed due to lack of activity docs M-T: Documentation work only

Comments

@szymon-szym
Copy link

Description

Hi all,

Thanks for a great framework!

It would be very useful if you could add to the documentation just a few examples of unit tests for bolt app. I suppose that writing tests is not that complicated if you have some experience, but it would be a great help for people who are new to mocking, stubbing etc.

What type of issue is this? (place an x in one of the [ ])

  • bug
  • enhancement (feature request)
  • question
  • documentation related
  • testing related
  • discussion

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.
@seratch seratch added the docs M-T: Documentation work only label Jan 27, 2020
@aoberoi
Copy link
Contributor

aoberoi commented Jan 29, 2020

Thanks for using our project and giving us your feedback!

I agree that some guidance around how to write tests for apps would be really helpful. We're making some progress towards establishing an easier technique in #353. Those changes will result in a Bolt v2, which we're hoping to release in a few weeks. So an update to our documentation about testing would make the most sense with or after that.

@RayBB
Copy link

RayBB commented May 21, 2020

Now that we're on version 2, can anyone share some examples of how they're doing unit testing?

@seratch
Copy link
Member

seratch commented May 22, 2020

This library hasn't come up with Slack feature-specific testing supports yet but this 3rd party project may be worth checking. https://github.com/IBM/slack-wrench/tree/master/packages

@szymon-szym
Copy link
Author

szymon-szym commented May 24, 2020

@RayBB a simple flow which works for me:

  • use a named functions in the listeners
  • call those functions in tests with a specific input
  • spy on mocked app web client and check args it was called with
  • match those args with a jest snapshot if needed

I can attach some code snippets if they may be useful

But for sure using the tool mentioned by seratch would be more straightforward

@RayBB
Copy link

RayBB commented May 24, 2020

@szymon-szym some examples would be appreciated!

@dschinkel
Copy link

dschinkel commented Jun 9, 2020

Here's an example of how to fake a real call. Keep it simple, you don't always need a mocking framework try simple js, it usually works and pays off.

// stub the real call
app.client.chat.postMessage = () => Promise.resolve({ called: true});

it('only sends one welcome message', async () => {
    app.client.chat.postMessage = () => Promise.resolve({ called: true});

    const channel = '';
    const user: SlackUser = {
      token: '',
      trigger_id: '',
      view: undefined,
      id: '',
      team_id: ''
    };

    const addedCrafter = {
      rows: [
        {
          crafter_email: '',
          crafter_name_first: '',
          crafter_name_last: '',
          crafter_slack_received_welcome_msg: false,
          crafter_slack_user_id: user.id
        }
      ]
    }

    fakeDB.client = createClient(addedCrafter)
    fakeDatabase(fakeDB.client);
    let response = await sendWelcomeDMMessage(channel, user);
    expect(response.called).toBe(true);

    addedCrafter.rows[0].crafter_slack_received_welcome_msg = true;
    fakeDB.client = createClient(addedCrafter)
    fakeDatabase(fakeDB.client);
    response = await sendWelcomeDMMessage(channel, user);
    expect(response.called).toBe(false);
  });

Code under test:

async function sendWelcomeDMMessage(channel: string, user: SlackUser) {
  const crafterModel: CrafterModel = {
    email: '',
    firstName: '',
    lastName: '',
    slackUserId: user.id,
    receivedWelcomeMsg: true
  }

  const crafter: CrafterModel = await add(crafterModel)
  
  if(crafter.receivedWelcomeMsg){
    return Promise.resolve({called: false });
  }

  return app.client.chat.postMessage( {
    token,
    text: 'Welcome!!!',
    channel: channel
  })
}

so rather than using something like nock to intercept a real uri which I couldn't get working with bolt but could with my own code not using bolt and just creating my own custom code, if you're testing your code it really don't matter how postMessage works. You don't want to go in depth mocking code you don't own. "Don't mock what you don't own" is a good rule to follow and follow that as much as you possibly can. In this case I got away with minimal stubbing of the 3rd party lib (bolt) by only having to stub postMessage. In this case the stub is a spy, and verifying that postMessage was called.

You just care that the code inside your function (your behavior) (in my case sendWelcomeMessage) works and this was really just spying to see if it sent a request via postMessage in this particular case.

Hope this helps anyone out there.

@szymon-szym
Copy link
Author

szymon-szym commented Oct 7, 2020

@RayBB It was a while, but still it might be useful
I am aware that there are strong opinions around mocking, choose whatever works for you :)

Here is a test:

/**
 * @jest-environment node
 */
/* eslint-disable*/
import SlackApp from '../app';
import * as HomeTab from '../handlers/app-home';
import { ExpressReceiver } from '@slack/bolt';
import { appHomeMainMenu } from '../handlers/app-home';
jest.mock('@slack/bolt/dist/ExpressReceiver');
jest.mock('../box_svc'); //<-- this is part of specific implementation of may SlackApp object, not relevant to this test

const mockClientWebPublish = jest.fn();
const mockClientUsersInfo = jest.fn();
jest.mock('../app', () => {
    return jest.fn().mockImplementation(() => {
        return {
            app: {
                client: {
                    views: {
                        publish: mockClientWebPublish,
                    },
                    users: {
                        info: mockClientUsersInfo,
                    },
                },
            },
        };
    });
});

let mockApp: SlackApp;

HomeTab.lib.getUserObject = jest.fn(() => {
    console.log('mocked get user called')
    return Promise.resolve({
        user: {
            profile: {
                first_name: 'test name',
            },
        },
    })
});

const fakeSlackArgs = {
    ack: () => {},
    body: {
        user: {
            id: '123',
        },
    },
    context: {
        botToken: 'ABC',
    },
    payload: {},
    view: {},
};

describe('Opening home tab', () => {
    beforeAll(async () => {
        const mockExpressReceiver = new ExpressReceiver({ signingSecret: '1234' });
        mockApp = new SlackApp(mockExpressReceiver);
    });
    it('Should publish expected view in the home tab', async () => {
        mockClientWebPublish.mockClear()
        await appHomeMainMenu(mockApp.app, (fakeSlackArgs as any))

        expect(mockClientWebPublish.mock.calls[0]).toMatchSnapshot()

    });
});


Here is a code to be checked:

export async function appHomeMainMenu(
    app: App,
    args: SlackActionMiddlewareArgs & SlackEventMiddlewareArgs & { context: Context },
): Promise<WebAPICallResult> {
    logger.info('NEW app_home_opened called');
    
    try {
        await args.ack();
    } catch (error) {
        logger.info(`app home called from the event - no ack() in the args ${error}`);
    }

    let userId: string;

    try {
        userId = args.body.event.user;
        logger.info(`getting user id from the event`);
    } catch {
        userId = args.body.user.id;
        logger.info(`getting user id from the action`);
    }

    logger.debug(`app home user Id: ${userId}`);
    /*eslint-disable-next-line @typescript-eslint/no-explicit-any */
    const userRes: any = await lib.getUserObject(app, args, userId);

    const userName: string = userRes.user.profile.first_name || '';
    logger.info('user data fetched');
    return await app.client.views.publish({
        token: args.context.botToken,
        user_id: userId,
        view: {
            type: 'home',
            callback_id: 'home_view',

            blocks: [
                --> snip <--
            ],
        },
    });
}

@SpencerKaiser
Copy link

I recently created an open source app called Asking for a Friend, which was written in TypeScript, lifted with Eslint and Prettier, and has 99% test coverage via Jest. I figured out a new pattern for the declaration of listener methods and it's worked really well for me when it comes to testing. Here's an example:

postAnonymousQuestion.ts (Functionality to test)

/* eslint-disable camelcase */
import { Middleware, SlackShortcutMiddlewareArgs, SlackShortcut } from '@slack/bolt';
import logger from '../../logger';
import { app } from '../../app';
import getRequiredEnvVar from '../../utils/getRequiredEnvVar';
import { getPostAnonymousQuestionModalBlocks } from '../blocks/postAnonymousQuestion';
import { callbackIds } from '../constants';

export const postAnonymousQuestion: Middleware<SlackShortcutMiddlewareArgs<SlackShortcut>> = async ({
  shortcut,
  ack,
}) => {
  ack();
  try {
    await app.client.views.open({
      token: getRequiredEnvVar('SLACK_TOKEN'),
      trigger_id: shortcut.trigger_id,
      view: {
        callback_id: callbackIds.postQuestionAnonymouslySubmitted,
        type: 'modal',
        title: {
          type: 'plain_text',
          text: 'Ask Question Anonymously',
        },
        blocks: getPostAnonymousQuestionModalBlocks(),
        submit: {
          type: 'plain_text',
          text: 'Ask Question',
        },
      },
    });
  } catch (error) {
    logger.error('Something went wrong publishing a view to Slack: ', error);
  }
};

postQuestionAnonymously.test.ts (Tests to cover above functionality)

/* eslint-disable camelcase, @typescript-eslint/no-explicit-any, import/first */
import 'jest';
import supertest from 'supertest';
import { createHash } from '../utils/slack';
import logger from '../../../logger';

const signingSecret = 'Secret';
process.env.SLACK_SIGNING_SECRET = signingSecret;
import { receiver, app } from '../../../app';
import { callbackIds } from '../../../slack/constants';

const trigger_id = '1234';
const mockShortcutPayload: any = {
  type: 'shortcut',
  team: { id: 'XXX', domain: 'XXX' },
  user: { id: 'XXX', username: 'XXX', team_id: 'XXX' },
  callback_id: callbackIds.postAnonymousQuestion,
  trigger_id,
};

const viewsOpenSpy = jest.spyOn(app.client.views, 'open').mockImplementation();
const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation();

describe('ignore action listener', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('handles the shortcut and opens a modal', async () => {
    const timestamp = new Date().valueOf();
    const signature = createHash(mockShortcutPayload, timestamp, signingSecret);
    await supertest(receiver.app)
      .post('/slack/events')
      .send(mockShortcutPayload)
      .set({
        'x-slack-signature': signature,
        'x-slack-request-timestamp': timestamp,
      })
      .expect(200);

    expect(viewsOpenSpy).toBeCalled();
    const args = viewsOpenSpy.mock.calls[0][0];
    expect(args.trigger_id).toEqual(trigger_id);
  });

  it("logs an error if the modal can't be opened", async () => {
    const timestamp = new Date().valueOf();
    const signature = createHash(mockShortcutPayload, timestamp, signingSecret);
    viewsOpenSpy.mockRejectedValueOnce(null);
    await supertest(receiver.app)
      .post('/slack/events')
      .send(mockShortcutPayload)
      .set({
        'x-slack-signature': signature,
        'x-slack-request-timestamp': timestamp,
      })
      .expect(200);

    expect(viewsOpenSpy).toBeCalled();
    expect(loggerErrorSpy).toBeCalled();
  });
});

Check out that project if you like the pattern above 🙂

@github-actions
Copy link

github-actions bot commented Dec 5, 2021

👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out.

@seratch seratch added auto-triage-skip Prevent this issue from being closed due to lack of activity and removed auto-triage-stale labels Dec 5, 2021
@misscoded misscoded mentioned this issue Feb 25, 2022
10 tasks
@filmaj
Copy link
Contributor

filmaj commented Sep 15, 2024

Going to reference this issue from #638 as worthwhile to consider when we tackle first-class testing support in bolt, but will close as duplicate of #638.

@filmaj filmaj closed this as completed Sep 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-triage-skip Prevent this issue from being closed due to lack of activity docs M-T: Documentation work only
Projects
None yet
Development

No branches or pull requests

7 participants