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.
x in one of the [ ])x in each of the [ ])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.
Now that we're on version 2, can anyone share some examples of how they're doing unit testing?
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
@RayBB a simple flow which works for me:
I can attach some code snippets if they may be useful
But for sure using the tool mentioned by seratch would be more straightforward
@szymon-szym some examples would be appreciated!
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.
@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 <--
],
},
});
}
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 馃檪
Most helpful comment
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)_postQuestionAnonymously.test.ts(_Tests to cover above functionality_)Check out that project if you like the pattern above 馃檪