【Jest】Slack APIをモックにしたテストを書いたのでメモ

スポンサードサーチ

サンプルコード

サンプルとして、
以下のようなチャンネルに(ボット, アプリ以外の)ユーザーがいるかを確認するコードを書いたとする

import { WebClient } from '@slack/web-api';

const getClient = (token: string) => {
  return new WebClient(token);
};

const getSlackChannelInfo = async (slackChannelId: string) => {
  try {
    const client = getClient(process.env.BOT_TOKEN );
    return await client.conversations.info({
      channel: slackChannelId
    });
  } catch (error) {
    console.log(`Failed to get slack channel info: ${error.message}`);
    throw error;
  }
};

const getSlackChannelMembers = async (slackChannelId: string) => {
  try {
    const client = getClient(process.env.BOT_TOKEN );
    return await client.conversations.members({
      channel: slackChannelId
    });
  } catch (error) {
    console.log(`Failed to get slack channel members: ${error.message}`);
    throw error;
  }
};

const getSlackUserInfo = async (slackUserId: string) => {
  try {
    const client = getClient(process.env.BOT_TOKEN );
    return await client.users.info({
      user: slackUserId
    });
  } catch (error) {
    console.log(`Failed to get slack user: ${error.message}`);
    return null;
  }
};

const postMessageToSlackChannel = async (
  message: string,
  slackChannelId: string
) => {
  try {
    const client = getClient(process.env.BOT_TOKEN );
    const params = {
      text: message,
      channel: slackChannelId
    };
    await client.chat.postMessage(params);
  } catch (error) {
    console.log(`Failed to send slack message: ${error.message}`);
    throw error;
  }
};

const isChannelActiveAndHasMembers = async (slackChannelId: string) => {
  try {
    const slackChannelInfo = await getSlackChannelInfo(channelId);
    const channel = slackChannelInfo?.channel;
    if (!channel || channel.is_archived) {
      return false;
    }

    const slackChannelMembers = await getSlackChannelMembers(slackChannelId);
    const slackUserIds = slackChannelMembers?.members || [];
    for (const slackUserId of slackUserIds) {
      const slackUserInfo = await getSlackUserInfo(slackUserId);
      console.log('slackUserInfo', slackUserInfo);
      const user = slackUserInfo?.user;
      if (user && !user.is_bot && !user.is_app_user && !user.deleted) {
        return true;
      }
    }

    return false;
  } catch (error) {
    return false;
  }
}

const sample = async (slackChannelId: string) => {
  const hasHumanUsers = await isChannelActiveAndHasMembers(slackChannelId);
  if (!hasHumanUsers ) {
    await postMessageToSlackChannel('エラーです', process.env.NOTICE_SLACK_CHANNEL_ID);
    return true;
  }

  return false;
}

テストコード

import { WebClient } from '@slack/web-api';

jest.mock('@slack/web-api', () => {
  return {
    WebClient: jest.fn(() => ({
      chat: {
        postMessage: jest.fn()
      },
      conversations: {
        info: jest.fn(),
        members: jest.fn()
      },
      users: {
        info: jest.fn()
      }
    }))
  };
});
const mockedWebClient = WebClient as unknown as jest.Mock;
const testSlackChannelId = 'C0xxxxxxxxx';

describe('サンプルテスト', () => {
  let mockSlackPostMessage: jest.Mock;
  let mockSlackChannelInfo: jest.Mock;
  let mockSlackChannelMembers: jest.Mock;
  let mockedSlackUserInfo: jest.Mock;

  beforeEach(async () => {
    process.env.BOT_TOKEN = 'dummy-token';
    mockSlackPostMessage = jest.fn().mockResolvedValue({ ok: true });
    mockSlackChannelInfo = jest
      .fn()
      .mockResolvedValue({ ok: true, channel: { is_archived: false } });
    mockSlackChannelMembers = jest.fn().mockResolvedValue({
      ok: true,
      members: ['mockUserId']
    });
    mockedSlackUserInfo = jest.fn().mockResolvedValue({
      ok: true,
      user: {
        is_bot: false,
        is_app_user: false,
        deleted: false
      }
    });
    mockedWebClient.mockImplementation(() => ({
      chat: { postMessage: mockSlackPostMessage },
      conversations: {
        info: mockSlackChannelInfo,
        members: mockSlackChannelMembers
      },
      users: { info: mockedSlackUserInfo }
    }));
  });

  it('正常系', async () => {
    await sample(testSlackChannelId);

    expect(mockSlackPostMessage).not.toHaveBeenCalled();
  });

  it('Slackチャンネルの情報が取得できない場合', async () => {
    mockSlackChannelInfo.mockRejectedValueOnce(
      new Error('conversations.info error')
    );

    await sample(testSlackChannelId);

    expect(mockSlackPostMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: process.env.NOTICE_SLACK_CHANNEL_ID
      })
    );
  });

  it('Slackチャンネルがアーカイブな場合', async () => {
    mockSlackChannelInfo.mockRejectedValueOnce({
      ok: true,
      is_archived: true
    });

    await sample(testSlackChannelId);

    expect(mockSlackPostMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: process.env.NOTICE_SLACK_CHANNEL_ID
      })
    );
  });

  it('Slackチャンネルにユーザーがいない場合', async () => {
    mockedSlackUserInfo = jest.fn().mockResolvedValue({
      ok: true,
      user: {
        is_bot: true,
        is_app_user: false,
        deleted: false
      }
    });

    await sample(testSlackChannelId);

    expect(mockSlackPostMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: process.env.NOTICE_SLACK_CHANNEL_ID
      })
    );
  });
});

ざっくり解説

  • WebClientをjest.Mockにするために、as unknown を挟む
const mockedWebClient = WebClient as unknown as jest.Mock;
  • デフォルトのレスポンスを用意しておく
mockedWebClient.mockImplementation(() => ({
  chat: { postMessage: mockSlackPostMessage },
  conversations: {
    info: mockSlackChannelInfo,
    members: mockSlackChannelMembers
  },
  users: { info: mockedSlackUserInfo }
}));
  • テスト内容に応じてレスポンス内容を書き換える
mockSlackChannelInfo.mockRejectedValueOnce(
  new Error('conversations.info error')
);

mockSlackChannelInfo.mockRejectedValueOnce({
  ok: true,
  is_archived: true
});

mockedSlackUserInfo = jest.fn().mockResolvedValue({
  ok: true,
  user: {
    is_bot: true,
    is_app_user: false,
    deleted: false
  }
});
  • 固定値での実行が決まっていれば、それで渡されているかを確認

Slack APIというよりはJestの書き方の話だけれど、下記の場合はclient.chat.postMessageのchannelが環境変数値で決まっいたパターン

expect(mockSlackPostMessage).toHaveBeenCalledWith(
  expect.objectContaining({
    channel: process.env.NOTICE_SLACK_CHANNEL_ID
  })
);

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。