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

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

おわりに

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

Discord BotをTypeScript/TDDで開発する(1)

最近、友人たちがDiscord Bot開発に興味を持ち、実際に開発しているのを見て、どうせだったら最近学習しているTypeScriptかつTDDで開発したいと思った矢先、[LIVE] Discord.js Unit Testing with Jest - YouTubeという素晴らしいYouTube動画を見つけたので、こちらの写経と動作確認を始めました。

まだ前半30分くらいですが、そこに至るまでのトラブルシューティングとつまづいたことをメモ、構成は以下の通り。

www.youtube.com

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

ソースコード

// index.ts
import { Message } from "discord.js"

export const messageHandler = async (message: Message) => {
    if (message.content === '!agile') {
        message.channel.send("やさしい")
    }
}
// index.spec.ts //テストファイル
import { Message } from "discord.js";
import { messageHandler } from "../src/handlers";

describe('Message Handler', () => {
    const message = ({
        channel: {
            send: jest.fn(), // よくわからない(1)
        },
        content: '!agile',
    } as unknown) as Message; // よくわからない(2)

    it('should call message handler', async () => {
        messageHandler(message);
        expect(message.channel.send).toHaveBeenCalledWith("やさしい");
    });
});

トラブルシューティング

自分の環境では「sh: ts-node: command not found」 と「/bin/sh: jest: command not found」というエラーに遭遇しましたが、それぞれ単純に「yarn add -D ts-node」、「yarn add -D jest」コマンドで回避できました(単純にYouTubeを見落とした可能性あり)。

つまづいたこと

jest.fn()

jest.fn()が公式ドキュメント(モック関数 · Jest)を読んでもピンとこなかったのですが、以下ブログを読んでようやく理解できた気がしました。

medium.com

一言でいえば、jest.fn()とは関数のモックです。
コードで説明すると、index.tsのmessage.channel.send()の実行時に、index.spec.tsでmessage.channel.send()にアサインされたjest.fn()が実行されます。
ただし、jest.fn()は何の処理もない空の関数です(jest.fn()の処理を定義したい場合は「jest.fn(() => "bar"」のように記述)。
ただ、空の関数ということは問題ではありません。
ここで確認したいことはmessage.channel.send()が"やさしい"という引数で実行されているかどうかなので、index.spec.tsで「expect(message.channel.send).toHaveBeenCalledWith("やさしい")」と確認しています。

as unknown as Message

コードを読んでいまいち理解できなかったことは、どうしてindex.spec.tsでMessageをunknown経由でMessage型に型アサーションしているかどうか、です。
これは割とすぐに理解できました。
ここでやりたいことは、Discord.jsのMessageのMockを作ることですが、本来Discord.jsのMessageは沢山のプロパティを持っています。ですがMockで全てのプロパティを定義すると大変かつコード可視性が低下するため、Mockでは必要分のプロパティを定義し、それでもMessage型として扱いたいのでunknown経由でMessage型に型アサーションしているということでした。

テストのMockを作る際の、unknown経由の型アサーションは便利ですね(きっと定番なんだと思います)。

book.yyts.org

時間があれば、さらに環境のセットアップやさらに進めた時につまづきなどがメモできればと思います!

大切なことに、あえて時間を掛けない

以前はワークショップやふりかえりで意見を挙げて貰う際、時間を多めに取っていた。じっくり考え、なるべく良い意見を出してもらいたいと考えているからだ。
ただ、先輩コーチから「余り時間を掛けない方が良い場合もある」とアドバイスを受けてから考えが変わってきた。
理由は2つある。
1つ目は、経過時間と意見の重要度は反比例するからだ。
つまり、最初に直感的に出した意見が、本人にとっては一番切実な意見であることが多く、その後はさほど切実ではない意見が挙がりがちだ。
(さらに、シンキングタイムが長ければ長いほど、時間を埋めるためだけの意見が挙がっていくことが多い)
2つ目は、意見が多く挙がれば挙がるほど、その後の共有に時間が掛かるからだ。時間を取ったせいで、どうでもいい意見が挙がり、その意見の共有に時間を取られワークショップやふりかえりが間延びするのは悪循環なのだ。

そのため、最近では、意見を挙げてもらう時間は必要そうな時間から-10〜20%した時間を設定するようにしている。(たとえば、5分必要そうならば、4分にする)

また、たとえばワークショップの最初にチーム分けをする時があるが、その際も以前は10分程度取っていたが、最近は1分程度にすることも多い。

「今から、(Miroのフレームに名前の付箋を用意し)今から1分で3チームに分かれてもらいます、お願いします!」

で、何とかなることが多い。あの人と一緒になりたいなどと余計なことを考えている暇がなく、とにかく動くしかないからだ。

そこで、たとえばふりかえりやワークショップでのシンキングタイムを半分にするという実験はどうでしょうか? むしろかえって良い意見が出るかもしれません。

スパゲティコードなので相対見積できません

アジャイル/スクラムの見積の考え方やプラクティスを納得してもらうのは難しい、といつも感じる。

www.youtube.com

理由の一つは、従来型の開発手法からスクラムにシフトしてきた開発者の多くが「見積とはマネジメントに対するコミットメントであり、ゆえに正確でなければならない」というマインドセットに深く染まっているからだ(僕もまだ完全に抜け切れてはいない)。

このようなケースでは、『アジャイルな見積りと計画づくり ~価値あるソフトウェアを育てる概念と技法』を引用しながらアジャイル/スクラムの見積の考え方やプラクティスを繰り返し説明し、実践している。
たとえば、見積はコミットメントではない、不確実性のコーン、計画を立てることは重要だが立てた計画は重要ではない、などなど。

上記の場合よりももっと厄介なのは「スパゲティコードなため、相対見積の考え方が適用できない」というケースだ。

アジャイル/スクラムでの見積の基本的な考え方は、絶対見積ではなく相対見積である。具体的にはフィーチャーA(≒機能)をWBSで分解して見積るのではなく、フィーチャーAとフィーチャーBのサイズを相対的に比較しながら見積をする。

だが、特に既存プロダクト改修でスクラムを実施する際、そのプロダクトがスパゲティコードの場合、相対比較の考え方を当てはめるのが難しい。

なぜならば、相対見積的にはフィーチャーAは3ポイントでフィーチャーBは5ポイントだったとしても、開発を始めたらフィーチャーAはグローバル変数を触るため広範に影響が及ぶことが判明し、結果10ポイントだった、ということが往々にして起きるからである。

こうして開発者が様々な痛い目をあっていくと、ますます開発者は防衛的になり、フィーチャー毎に時間を掛けて絶対見積するようになり、アジャイル/スクラムのメリットが失われてゆく......

さて、このような場合、アジャイルコーチとして何とアドバイスすれば良いか、自問自答してみた。

短期的には、以下のような方法が考えられるかもしれない。

  • プロダクトバックログアイテムをより小さく分割する
  • 影響調査をプロダクトバックログアイテムとして切り出す(スパイク)
  • スプリントでスパゲティコードのために追加されたタスクを記録しておき、記録に基づいてスプリントのバッファとして確保する

ただし、中長期的には、問題を根本から断つために、チームや組織に以下を実施するコストを割いてもらうように働きかける必要がある。

以下は2013年の記事からの引用だ。

コードの構造はなぜ重要なのでしょう? そのコードが10年後、スパゲティコードのおかげで内容や機能を変更できなければ、変更や追加が可能だというソフトウェアが持つ大事な価値を失ってしまうのです。

www.publickey1.jp

約10年後の今、膨大なスパゲティコードをスタート地点に、スクラムを始めたいというチームは沢山いると、思う。
皆さんはどのようにしてこの問題に取り組んでいるのか、聞いてみたい。

どのようにチームを分けたら良いですか?

最近あるチームから人数が多すぎるからチームを分割したいが、どのようにチームを分けたら良いですか? という相談が来た。先輩のアジャイルコーチと私は(定番の回答となりつつあるが)、まず自分たちでスキルマップを作り、それから、スキルマップを見ながら自分たちでチームを決める、ということを提案した。
ポイントは、自分たちでチームを決める、ということかもと漠然と考えていたら、今日ようやく読書を再開した『組織パターン (Object Oriented SELECTION)』に「自分たちで選んだチーム」というパターンが紹介されていた。

4.2.11 | 自分たちで選んだチーム **
組織を細かくする(4.2.2.)に従うと、少人数でえり抜きのチームが必要になる。そのようなチームメンバーはどのように決めればよいのだろう?

チームのダイナミクスは、任命されたメンバーで構成されている場合に最も低くなる。
(中略)
それゆえ:
 自分たちでチームメンバーを選ばせるようにして、熱心なチームを作ろう。チームメンバーの実力や幅広い趣味という観点から、限定的な審査を行おう
 このようなチームは、常にとは言わないまでも、意志の力が強いことが多い。

......流石、『組織パターン』、大切なことはすべて書いてある。
最初のチーム編成はチーム自身で決めるのは難しいかもしれないが、大規模になってチームを分割するタイミングは自分たちでチームを構成する良いチャンスだと思うので、そのような状況の方は是非試してみてください。

さて、晴れて自分たちで選んだチームが誕生しても、とりわけリモート時代、怖いのがチームのサイロ化だ。というわけで分割されたチームにしつこく伝えるのは、あなたたちは仕方なく分割しただけで、本当は同じユーザーに同じプロダクトを届けるための1チームなのだ、ということ。なので、初めから大規模スクラムなんかを参考にしてチーム毎にスクラムイベントを分割するのも考えものだと最近思う。むしろ、初めのうちは、チーム同士がなるべく一緒に活動して、不都合が目立つ部分から徐々に別れるのが良いのでは、と伝えている。
(なんだか未練タラタラのカップルみたいだけど)
もちろん、チーム同士が進化し、お互いに学びあうようになれば、それはそれで最高だ。

閑話休題

今日見たYouTube、スピ系だけど、ブログを書くことを励まされたのでシェア。表現は、ブログは、集客や仕事ではなく自分の為に書くんですよね。

www.youtube.com

チームの雰囲気を悪くするアジャイルコーチ

ブログを再開したきっかけは2つある。

ひとつ目は、偶然観た「人生を変える情報は偶然からやってくる」という勝間和代氏のYouTubeだ。
以前は、誰かと同じ意見を言うのは無意味だと思っていた。
けれどもこのYouTubeを観て、誰がいつ何に触れるかはわからない、ので、たとえ意見が被ったとしても、このブログでその意見に触れるかもしれない誰かのために書くことは意味があると思い直した。

www.youtube.com

ふたつ目は、エドガー・シャインの著作における「ワンアップ」という言葉でググった際、偶然目に止まった竹端寛氏のブログだ(これも偶然)。

竹端氏はブログで、様々な書籍を引用しつつ、自身の体験談や意見、そして感情を縦横無尽に綴っている。
こんなブログが書けたら。
それに、僭越ですが、こういうやり方ならば毎日書けるかも、とも思った。

surume.org

それでも、書籍を引用して、そこからのインスピレーションや自分の感情や思考を書くというスタイルは、「我田引水」ではないかという後めたさもあった。
けれども、今日、『プレイフル・シンキング[決定版] 働く人と場を楽しくする思考法』を読み、「創造的借用(creative appropriation)」という概念を知り、ずっと抱えていた後ろめたさが消えた。
そう、自分がブログでやりたいのは創造的借用なのだ。

また、アウトプットは学びそのものの行為でもある。よく、知識や情報を「インプット」することが学びだと誤解されがちだが、それは違う。アウトプットする過程において、インプットした知識や情報を自分なりに咀嚼し、意味の組み替えや再構成を行うことで自分のものにしていくことができるのである。これを「創造的借用(creative appropriation)」という。

さて、本書の「プレイフル」というキーワードとアジャイルコーチを強引に託ければ、アジャイルコーチが一番やってはいけないことは「チームの雰囲気を悪くすること」ではないかと思うようになった。

個人的に気をつけていることは、たとえば、成果がなかなか出なかったり、失敗したチームを追い詰めないこと(失敗は気づきの貴重な種だ)。チームができないことではなく、できることを探すこと。チームの悪い雰囲気に飲まれ、自分自身まで感情的にならないこと。
そんな時は、努めて明るく、気楽に以下のようなことを問いかけてみる。

「何か雰囲気が暗くないですか」
「うまくいっていないようですね」
「一緒にやってみませんか」
「休憩しませんか」

【TypeScript】ジェネリクス学習メモ

TypeScriptのジェネリクスについての学習メモ(ほとんど以下サイトのパラフレーズです)

book.yyts.org

ジェネリクスが解決する問題

以下のような、同一処理だが異なる引数の型の関数が作りたい場合、引数をany型にして関数をまとめるとTypeScriptの型安全というメリットが失われてしまう(が、そのまま実装すると同じような処理が重複してしまう)

// 重複した3つの関数
function chooseRandomlyString(v1: string, v2: string): string {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyNumber(v1: number, v2: number): number {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyURL(v1: URL, v2: URL): URL {
  return Math.random() <= 0.5 ? v1 : v2;
}

ジェネリクス

そこで、以下chooseRandomly()関数のように型を引数として扱えるようにするのがジェネリクスである。
ちなみに、<T>を型変数と呼び、慣習的にTを利用することが多いらしいですが、AでもTypeでも構わないとのこと。
また、引数の型も型推論できるため、型が自明な場合は呼び出す際にstringやnumberなどの型指定を省略可能とのこと。

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>("勝ち", "負け");
chooseRandomly<number>(1, 2);
chooseRandomly<URL>(urlA, urlB);

ジェネリクスが利用されるシーン

多くの種類の型でも機能するように、Arrayなどの多くの標準ライブラリでジェネリクスが利用されている(以下はArray.prototype.mapの実装例)

Array.prototype.map()
const textNumbers = ["1", "2", "3", "4"];
const numbers = textNumbers.map<number>(function(text: string) {
    return Number(text);
});