アジャイルコーチの備忘録

3歩歩いたら忘れるニワトリアジャイルコーチの備忘録。書評、活動記録など...

【備忘録】Discord BotをTypeScript/TDDで開発する(2)

前回のブログ(Discord BotをTypeScript/TDDで開発する(1) - アジャイルコーチの備忘録)の続きです、[LIVE] Discord.js Unit Testing with Jest - YouTubeを観終わったので、今日もつまづいたことを書いていきます。

f:id:norihiko-saito-1219:20210718160244j:plain

ソースコード

export const guildMemberAddHandler = (member: GuildMember) => {
    const { guild } = member;
    try {
        const role = guild.roles.cache.get('23133131231');
        if (!role) throw 'Role not found.';
        member.roles.add(role);
    } catch(err) {
        console.log(err);
        const channel = <TextChannel>guild.channels.cache.get('14323232424');
        if(!channel) return null;
        channel.send('The role was not added to the member because it was not found.');
    }
};
//テストコード
import { Collection, Guild, GuildChannelManager, GuildMember, GuildMemberRoleManager, Role, TextChannel } from 'discord.js';
import { guildMemberAddHandler } from '../src/handlers';

describe('GuildMemberAdd Handler', () => { 
    const cacheMock: Collection<any, any> = ({
        get: jest.fn(),
    } as unknown) as Collection<any, any>;

    const guildMemberRoleManager: GuildMemberRoleManager = ({
        add: jest.fn(),
        cache: cacheMock,
    } as unknown) as GuildMemberRoleManager; 

    const guildChannelsManager: GuildChannelManager = ({
        cache: cacheMock,
    } as unknown) as GuildChannelManager;

    const guildMock: Guild = ({
        roles: guildMemberRoleManager,
        channels: guildChannelsManager,
    } as unknown) as Guild;

    const memberMock: GuildMember = ({
        roles: guildMemberRoleManager,
        guild: guildMock,
    } as unknown) as GuildMember; 

    const TextChannelMock: TextChannel = ({
        send: jest.fn(),
    } as unknown) as TextChannel;

    const roleMock: Role = ({
        id: '',
    } as unknown) as Role;

    it('should add a role to the member', async () => {
        jest.spyOn(guildMock.roles.cache, 'get').mockImplementationOnce((): any => roleMock); //よくわからない
        await guildMemberAddHandler(memberMock);
        expect(memberMock.roles.add).toHaveBeenCalledTimes(1);
    });

    it('should throw an error when role is not found', async () => {
        try {
            await guildMemberAddHandler(memberMock);
        }catch(err) {
            expect(err).toBeDefined();
            expect(guildMock.channels.cache.get).toHaveBeenCalledTimes(1);
        }

    });
});

今回つまづいたことは、一箇所(「jest.spyOn(guildMock.roles.cache, 'get').mockImplementationOnce((): any => roleMock);」)です。

jest.spyOn()

さて、前回はjest.fn()だけを使っていたのに、今回はjest.spyOn()を利用して関数のmockを作成しています。
まずjest.spyOn()は何かというと、オブジェクトの関数をmockする関数です(ここでは、guildMock.roles.cacheオブジェクトのget関数をmockするという意味)。
そして、mockImplementationOnce()でcacheMockのget関数のmockをさらに上書きしています。

上書き、ということがミソで、cacheMockのgetを必要に応じて上書きしたいので、今回jest.spyOn()とmockImplementationOnce()を利用しています。
なぜなら、テスト対象のソースコードではtry節ではguild.roles.cache.getはRoleを返すように、catch節ではguild.roles.cache.getはTextChannelを返すように、それぞれcacheMockのgetのmock処理を実装する必要があるからです。
そのため、それぞれのテストメソッドでjest.spyOn()とmockImplementationOnce()で関数をさらに上書きしていた、ということでした。

jestjs.io

おわりに

今回はとても簡単ですが見終わってつまづいたことを整理しました。
次回は今更ですが、環境のセットアップを書くかもしれません。。