mizdra's blog

ぽよぐらみんぐ

msw で handler の mock や spy をする

元々「ServiceWorker をベースにした技術をわざわざテストに持ち込む意味とは?」と思って、msw をテスト環境で使う意義について懐疑的だったのですが、いざ使ってみるとすごく便利ですね。ServiceWorker 云々以前に、ネットワークリクエストの mock ライブラリとして、インターフェイスがとても使いやすいです。

zenn.dev

特に handler の mock や spy が簡単にできるのが嬉しいです。Jest と組み合わせた時の例だと、以下のようなイメージ。

// jest.setup.ts
import { server } from './src/test-utils/msw';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// src/test-utils/msw.ts
import { setupServer } from 'msw/node';
import { graphql, rest } from 'msw';

export type TodoAddRequest = { title: string };
export type TodoAddResponse =
  { type: 'success' }
  | { type: 'error', error: string };

const handlers = [
  // デフォルトの handler をここで定義しておく。
  rest.post<TodoAddRequest>('/todo/add', (req, res, ctx) => {
    return res(
      ctx.json<TodoAddResponse>({ type: 'success' }),
    );
  }),
  // ...
];

export const server = setupServer(...handlers);
// src/components/TodoList.test.ts

import { server, TodoAddResponse } from '../test-utils/msw';

// mock の例

test('todo 追加時にエラーになったら、エラーメッセージが表示される', async () => {
  server.use(rest.post('/todo/add', (req, res, ctx) => {
    // 既に `/todo/add` 向けに登録されている handler を無視して、
    // この handler で上書きする (mock)
    return res(
      ctx.status(500),
      ctx.json<TodoAddResponse>({
        type: 'error',
        error: 'TODO は 10 個までしか登録できません。',
      }),
    );
  }));

  render(<TodoList />);
  
  await userEvent.type(screen.getByLabelText('title'), '買い物に行く');
  await userEvent.click(screen.getByRole('button', { name: '追加' }));

  expect(screen.getByRole('alert')).toHaveTextContent(
    'TODO は 10 個までしか登録できません。',
  );
});

// spy の例

test('todo を追加できる', async () => {
  const todoAddSpy = jest.fn();
  // これを `server.use` に渡す。 `return res(...)` していないので、
  // デフォルトの handler まで貫通する
  server.use(rest.post('/todo/add', todoAddSpy));

  render(<TodoList />);
  
  await userEvent.type(screen.getByLabelText('title'), '買い物に行く');

  expect(todoAddSpy).not.toBeCalled();
  await userEvent.click(screen.getByRole('button', { name: '追加' }));

  // handler が期待通りの呼ばれ方をしたかをチェック
  expect(todoAddSpy).toBeCalled();
  expect(todoAddSpy).toBeCalledWith(expect.objectContaining(
    body: {
      title: '買い物に行く',
    },
  ));
}

テストケース側で server.usereturn res(...) を含む handler を登録すれば mock が、return res(...) を含まない handler を登録すれば spy ができます*1。便利ですね。

*1:よく考えてみたら、デフォルトの handler の入力を傍受できるだけで出力は傍受できないので、spy と言えるかどうか微妙な気がしてきた...

過去に git clone した OSS リポジトリを一覧する

ということをふと思いついた。定量的に物事を見れると良い振り返りができて嬉しいと思う。とりあえず集計だけでもやってみる。

shell の history から一覧を作る

どうやって一覧しよう、と思ってまず思いついたのが、shell の history を使う方法。ghq list で探しても良いけど、ディスク容量が減ってきたら見なくなったリポジトリを削除したりするので、やっぱり shell history からたどるのが良いだろうというアイデア。

history 1 の出力を grep すれば良いだけで、ちょっと前にも id:onk さんが似たようなことをされていた。

onk.hatenablog.jp

$ history 1 | awk '{ print $2,$3,$4 }' | grep -e 'git clone' | awk '{ print $3 }'
git@github.com:mizdra/typed-less-modules.git
git@github.com:vercel/next.js.git
git@github.com:madyankin/postcss-modules-example.git
...

普段は git clone ではなく ghq get を使っているので、OR 検索する。

$ history 1 | awk '{ print $2,$3,$4 }' | grep -e 'git clone' -e 'ghq get' | awk '{ print $3 }'
git@github.com:mizdra/typed-less-modules.git
git@github.com:vercel/next.js.git
git@github.com:madyankin/postcss-modules-example.git
git@github.com:mrmckeb/typescript-plugin-css-modules.git
--help
--shallow
...

ghq get --shallow <url> のようなケースが上手く考慮できていないので考慮してやる。ついでに --help は除外する。

$ history 1 | awk '{ print $2,$3,$4,$5 }' | grep -e 'git clone' -e 'ghq get' | grep -v '\-\-help' | awk '{ print $3,$4 }'
git@github.com:mizdra/typed-less-modules.git
git@github.com:vercel/next.js.git
git@github.com:madyankin/postcss-modules-example.git
git@github.com:mrmckeb/typescript-plugin-css-modules.git
--shallow git@github.com:vercel/next.js.git

id:mizdragit sclone という git clone --shallow の alias も使っているので、これも考慮する。

$ history 1 | awk '{ print $2,$3,$4,$5 }' | grep -e 'git clone' -e 'ghq get' -e 'git sclone' | grep -v '\-\-help' | awk '{ print $3,$4 }'
git@github.com:mizdra/typed-less-modules.git
git@github.com:vercel/next.js.git
git@github.com:madyankin/postcss-modules-example.git
git@github.com:mrmckeb/typescript-plugin-css-modules.git
--shallow git@github.com:vercel/next.js.git
git@github.com:garris/BackstopJS.git
...

自分以外の OSS のリポジトリのみに絞りたいので、org が自分のものは除外する (OSS を自分の org に fork したものも除外されてしまうけど…まあそんな数もないので気にせずで)。こういう時ハンドルネームがユニークだと、雑に除外条件を設定しても期待通りになってうれしい。

$ history 1 | awk '{ print $2,$3,$4,$5 }' | grep -e 'git clone' -e 'ghq get' -e 'git sclone' | grep -v -e '\-\-help' -e 'mizdra' | awk '{ print $3,$4 }'
git@github.com:vercel/next.js.git
git@github.com:madyankin/postcss-modules-example.git
git@github.com:mrmckeb/typescript-plugin-css-modules.git
--shallow git@github.com:vercel/next.js.git
git@github.com:garris/BackstopJS.git
...

完成系

という訳で、完成したワンライナーとその実行結果がこちら。private リポジトリ見られると良くないものや、mizdra 以外の org で自分が開発しているものなどは手作業で除外した。

$ history 1 | awk '{ print $2,$3,$4,$5 }' | grep -e 'git clone' -e 'ghq get' -e 'git sclone' | grep -v -e '\-\-help' -e 'mizdra' | awk '{ print $3,$4 }'
git@github.com:vnc0/magic-trackpad-switcher.git
git@github.com:mrmckeb/typescript-plugin-css-modules.git
git@github.com:AriPerkkio/eslint-remote-tester.git
git@github.com:zhouzi/graphql-codegen-factories.git
git@github.com:garris/BackstopJS.git
git@github.com:APIs-guru/graphql-faker.git
git@github.com:isucon/isucon11-qualify.git
git@github.com:vercel/next.js.git
--shallow git@github.com:vercel/next.js.git
git@github.com:vercel/next.js.git
git@github.com:jest-community/vscode-jest.git
https://github.com/jest-community/jest-editor-support.git
git@github.com:prettier/prettier.git
https://github.com/royriojas/file-entry-cache
--shallow git@github.com:microsoft/TypeScript.git
--shallow git@github.com:dotansimha/graphql-code-generator.git
git@github.com:madyankin/postcss-modules-example.git
git@github.com:facebook/docusaurus.git
git@github.com:postcss/postcss.git

同じリポジトリが何回も出力されていたり、--shallow が出ていたりするのが気になるけど、とりあえず良さそうなデータが集計できて満足した!

shell history だけだと clone 日時まではわからないので、なんか色々工夫したほうが良いのかもしれない。HISTTIMEFORMAT を変更して shell history に日付が埋め込まれるようにするのが良い気がするけど、これって過去の履歴壊れたりしないかな…どうしようかな…というところで悩み中。

takami-hiroki.hatenablog.com

おまけ

ところで一覧を見て、「なんか最近触ったリポジトリばかりだな…」と思ったのでちょっと調べてみたら…

$ history 1 | wc -l
    1068
$ echo $HISTSIZE
2000
$ cat ~/.zshenv | grep HISTSIZE
export HISTSIZE=1000000

.zshrc で設定した $HISTSIZE が上手く適用されていないのか、1068 件しか保存されてなさそう。というか値 3 つともバラバラになっている。そんな………………

追記 (2022/08/06 02:12)

どうやら shell history の最大件数は HISTSIZE ではなく HISTFILESIZE で調整するものらしい。

oplern.hatenablog.com

$ echo $HISTFILESIZE

$ cat ~/.zshenv | grep HISTFILESIZE
$ echo $?
1

何も設定していなくて、zsh のデフォルト値が使われていそう。そういうことだったのか…

追記 (2022/08/06 12:23)

もうちょっと調べてみたら、zsh で履歴の最大保存件数を表す環境変数は HISTFILESIZE ではなく SAVEHIST のようだった。これ shell ごとに違うんですね。

timesaving.hatenablog.com

$ echo $SAVEHIST
1000
$ cat ~/.zshenv | grep SAVEHIST
export SAVEHIST=1000000

.zshenvSAVEHIST 設定しているはずなのだけど、なぜか無視されていた。.zshenv で設定している他の環境変数 (export EDITOR="code --wait" とか) はちゃんと認識されているのだけどなー。なんでだろう。

追記 (2022/08/06 13:26)

メーリングリストで何か議論されていないかな…と思ってメーリングリストのアーカイブを検索してみる。アーカイブは https://www.zsh.org/mla/ にあるけど、全文検索機能は付いていないので、ここは Google で <キーワード> site:https://www.zsh.org/mla/ というクエリを打ち込んで調べる。Google 便利。

という訳で ghq get git@github.com:zsh-users/zsh.git する。こうして git clone した OSS リポジトリがまた1つ増えていく…

もう追う体力残ってないので、.zshrc に設定を移動して終わりにした。

ISUCON 12 予選に出場した

id:odan3240 さんと「情報処理部」というチームで出場してきました。

id:odan3240 さんの参加記もあるので合わせてどうぞ:

odan3240.hatenablog.com

結果

  • 最終スコア: 3270

何もできなかった...

時系列

結構前の話なのでうろ覚えだけど…大体こういう流れだったと思う。

  • 10:00 競技開始
  • 10:30 インスタント立てたりマニュアル読み合わせしたり
  • 〜11:30 ソースコード取り出したり、デプロイスクリプト書いたり、Datadog 入れたり
  • 11:30〜
    • id:odan3240 N+1 修正、インデックス貼る
    • id:mizdra sqlite から mysql に移行し始める
  • 14:00
  • 15:00
  • 17:30
    • Redis 導入も mysql 移行も間に合わない!となって、監視設定切ったりパラメータチューニングしてベンチマーク回し始める
    • 3270 点出たところで終了

やったこと

github.com

  • sqlite のスキーマに index を貼った
    • ...のだけど、途中でやっぱり sqlite 脱出しよう!となって中断して、実際に貼ったのは 1 つだけ。
  • sqlite のデータを mysql に移行する (失敗)
    • 最初 sqlite3-to-sql を使って移行を始めたけど、40分くらい? (よく覚えていない) 掛かる上に、完走した後に player_score テーブルのレコード数を数えたら、20万件くらい欠けていることが判明した
      • 虚無へと消えるレコード...
      • 警告とかエラーとかも出てなくて、何が起きていたのかさっぱり分からなかった
      • 「なんで〜」となって試行錯誤しているうちに 1 時間くらい溶けていた
    • そもそも 40 分掛かるのおかしいよね、ということで CSV に変換してから import する方法に切り替え始めた
      • for file in `ls ../../initial_data/*.db`; do
          sqlite3 -csv $file "select * from competition;" >> competition.csv
          sqlite3 -csv $file "select * from player;" >> player.csv
          sqlite3 -csv $file "select * from player_score;" >> player_score.csv
        done
        mysql -uisucon -pisucon -Disuports --enable-local-infile -e"load data local infile 'competition.csv' into table competition fields terminated by ',' OPTIONALLY ENCLOSED BY '\"';";
        mysql -uisucon -pisucon -Disuports --enable-local-infile -e"load data local infile 'player.csv' into table player fields terminated by ',' OPTIONALLY ENCLOSED BY '\"';";
        mysql -uisucon -pisucon -Disuports --enable-local-infile -e"load data local infile 'player_score.csv' into table player_score fields terminated by ',' OPTIONALLY ENCLOSED BY '\"';";
        
      • これで 20 分くらいに短縮された
      • けどレコードが虚無へと消えていくのは直らない...
    • mysql のバッファに余裕がなくてレコードが欠けてしまっているのでは? と思って、mysql のパラメータチューニングをし始める
      • [mysqld]
        innodb_buffer_pool_size=4G
        innodb_buffer_pool_instances=4
        innodb_log_buffer_size=200MB
        innodb_log_file_size=400M
        innodb_flush_method=O_DIRECT
        max_allowed_packet    = 64M
        
      • これで 3 分くらいに短縮された
      • レコードが虚無へと消えることもなくなった!
    • しかしベンチマークを回してみると POST /initialize がタイムアウトしてしまう!
      • webapp/sql/init.sh にこういう初期化用の sql を書いていたのだけど、created_at レコードに index がなくて、そもそも webapp/sql/init.sh に時間がかかりすぎるらしい
      • index 貼った
    • まだタイムアウトする!
      • GET /api/admin/tenants/billing が遅すぎてタイムアウトしてた
      • N+1 直してなかったのが原因っぽかった
      • 直そう、と思ったけど残り 30 分ほどしかなくて、ここであえなく時間切れ
      • https://github.com/odanado/isucon12-qualifier/pull/9
  • その他 import 中の待ち時間に nginx 周りでなにかできることないか探したり、シュッとできそうだったので /api/me をゲストユーザの時は nginx から返すようにしたりしたけど、ほとんど点数は変わらなかった

感想

sqlite to mysql に手を出したのが良くなかった… 危険な匂いを嗅ぎ分けて、先に他のボトルネック (それこそ最後に詰まっていた N+1 とか) から 1 つずつ潰していけばよかった。あとデータ量が多い時の mysql のパラメータチューニングの方法とかも事前に練習しておけばもっとスムーズだったかも。というよりは、業務でデータ量多い DB を扱う経験を積んだりとか、普段から色々なことに手を伸ばしていきましょうという話っぽい。もっと挑戦していきたい。

来年もがんばります。

趣味と仕事と技術検証

id:mizdra はプログラミングが趣味で、趣味と仕事が一致している。そのため、趣味でプログラミングする時は常に仕事を見据えた技術検証をしている。

趣味のついでに仕事にも役立てられたら最高だよね、という意識がある。

ところで、ちょっと別の話題だけど、趣味プログラミングで他に心がけていることとして、「常に何かしら新しいことに挑戦する」というものがある。

普段趣味プログラミングで何か作る時、何かしら新しいことに挑戦するということを意識している ... 折角何か創作活動をするので、ついでに新たなことに挑戦し、新たな学びやスキルの向上へと繋げようという狙い

from: 趣味で創作する時は常に何かしら新しいことに挑戦する - mizdra's blog

折角趣味でやるんだから、お得度が最大になるような時間の使い方をしよう、という意識がここにも現れている。面白い。

遠い未来の話をする理由、それは自信を持って前に進むため

id:mizdra は時々仕事で「今の時点で結論を出す必要はない、遠い未来の話」をすることがあります。1年後に考えれば良いことを、今考える、とかです。それをするのは何故か、という話を今日はします。

長期プロジェクトを段階的に進めていくには、不安が付き物

現在 id:mizdra はレガシーな Web アプリケーションへの Next.js の導入を仕事でやっています。新規実装部分から少しずつ Next.js を導入していて、段階的に Next.js 化を進めています。全部一気に Next.js 化するとなると、「レガシーな技術スタックでやってたアレは Next.js/React でどうやるの?」という問いが絶え間なく発生して全く前に進めなくなります。しかしスコープを小さくすれば、少しずつ問いに答えていって、進行できます。途中で方針転換もしやすいし、最初の機能をリリースするまでの時間も短くなります。プロジェクト全体のリスクを下げるためにも、段階的に進めていくことはとても重要です。

一方で段階的に進める場合、最初から全ての方針が決まりきっている訳ではないので、不安に駆られることがあります。最初のフェーズで先送りにした問題は、結局後で向き合うことになります。スコープを小さくするためには必要なことなので、それ自体は悪くないです。ただ、こうした疑問も抱くと思います。

『先送りにして、ちゃんと後から解決できるのか?』

結局後から解決できないと、全部イチからやり直す必要があるのではないか。今やっている作業が全部無駄になるのではないか。そうした不安に駆られます。その結果、日々の作業や意思決定に迷いが生じたり、手が止まってしまったり、議論が発散して決着が着かなくなってしまいます。不安は、目に見えない形でプロジェクトの進行によくない影響を与えていくことになります。だから「そうした不安を感じることもあるよね」で済ませるのではなく、ケアしていくことが重要です。

見通しを良くすることが大事

この問題をケアするには、いくつかの方法があります。例えば「技術ロードマップを敷く」とかです。Next.js 化で言えば、この順で問題を解決していって、古いページを Next.js 化していけばスムーズに進めるだろう、みたいな長期的な計画を立てます。ロードマップを描くと、課題に取り組むタイミングを皆がイメージできるようになって、安心感が生まれます。

また、まだ結論が出し切れていない細部の設計を紹介することも有効です。「先送りにして、ちゃんと後から解決できる保証は当然ないのだけど、何も考えていない訳ではなくて、多分できますよ、それなりに自信がありますよ。」という意思表明でもあります。たとえ荒削りのものであっても、これがあるのと無いのとでは、安心感が全然違います。

まとめると、先行きに対する見通しを良くすることが大事です。

見通しが良いと、自信を持って前に進める

こうして見通しが良くなると、日々の作業や意思決定に迷いが生じることもなくなり、結論も出しやすくなります。なにより自信を持って長期プロジェクトを進行できるところが大きいと思います。長期に渡ってずっと不安に駆られるのは大変ですからね。

まとめ

  • 長期プロジェクトを段階的に進めていくには、不安が付き物
  • 未来への見通しを良くすると、不安を軽減できる
    • 技術ロードマップを敷く
    • 結論が出し切れていない細部の設計をあえて紹介する
  • 見通しが良ければ、自信を持って前に進める
  • 段階的に進めることと、未来への見通しを良くすることは直交する概念ではない
    • 一気に全部やるわけではないけど、未来も見据える
    • バランスを取ることが大事

おまけ

この記事で書いたことと全く同じ話が、以下の記事でも軽く触れられていて、良いなと思ったので紹介します。Rust の実験的機能「GATs (generic associated types)」という機能があるのですが、その一部をそろそろ安定化させますよ、という内容の記事です。既存の全ての言語機能と一気に結合するのが難しく、段階的に結合と安定化をしようとしていて、それに対してコミュニティから「安定化を後回しにした部分は、後から問題が出て安定化できなくならないか?」「安定化させるなら、全部一気に安定化できるまで待つべきでは?」という疑問が出ていて、それに対するアンサーが書いてあります。

The goal of this blog post is to try to imagine a “shiny future” where we have GATs. It’s to consider a couple of the patterns that people want GATs for, how we might want to integrate GATs into standard library traits, and a few issues that don’t work today that we want to work in the future. We want this so that we have a future to strive for after stabilization. There are known shortcomings with GATs right now, but that’s okay. They’re already powerful and, as you’ll see, we have some thoughts on how to make them more powerful and ergonomic in the future.

DeepL 翻訳:

このブログ記事の目的は、GATがある「輝やかしい未来」を想像してみることです。人々がGATを欲しがっているいくつかのパターン、>GATを標準ライブラリのtraitに統合する方法、そして現在機能していない問題のうち、将来的に機能するようにしたいいくつかの問題について考えることです。これは、安定化した後に目指すべき未来があるようにしたいのです。今、GATには既知の欠点がありますが、それでもいいんです。すでにパワフルですし、ご覧のように、将来的にもっとパワフルで人間工学に基づいたものにするための考えもあります。

「try to imagine a “shiny future” (輝かしい未来を想像してみる)」「We want this so that we have a future to strive (目指すべき未来があるようにしたい)」という言葉がとっても良いと思います。

リリースノート自動生成テクニック

普段からいくつか趣味で作ったツールやライブラリを npm パッケージとして publish しています。ちょっと工夫していることとして、「できるだけ簡単に npm publish できるようにしておく」というものがあります。npm publish が心理的に、手順的に難しいと、すでに main ブランチに新機能や修正が入っているのに、npm publish されていない、という状況が発生しがちです。新機能や修正をすぐにユーザに送り届けられるよう、npm publish は無思考でできるようになっていると嬉しいです。

その一環として、リリースノート (CHANGELOG) の自動生成というのをやっているので、その紹介をしてみます。本当は 6 月にやっていた Maintainer Month 期間 に間に合わせたかったのですが、とろとろしていたら 7 月になってしまった! まあ遅れたから公開しないのも勿体ないので気にせず公開します。

GitHub のリリースノートの自動生成機能を使う

github-changelog-generator とか、git-chglog とか、自動生成するツールはすでに色々あるのだけど、id:mizdra は最近 GitHub の「Automatically generated release notes」という機能を使っている。

まず事前準備として、.github/release.yml というファイルをリポジトリに commit しておきます。この label がついていたらこのグループに振り分ける、みたいな設定を書きます。

# .github/release.yml
# ref: https://github.com/mizdra/eslint-interactive/blob/ae140c92081f8fb5c6d2cc3368ed5b186529981f/.github/release.yml

changelog:
  exclude:
    labels:
      - 'ignore for release'

  categories:
    - title: Breaking Change
      labels: ['Type: Breaking Change']

    - title: Bug
      labels: ['Type: Bug']
    - title: Documentation
      labels: ['Type: Documentation']
    - title: Feature
      labels: ['Type: Feature']
    - title: Refactoring
      labels: ['Type: Refactoring']
    - title: Testing
      labels: ['Type: Testing']
    - title: Maintenance
      labels: ['Type: Maintenance']
    - title: CI
      labels: ['Type: CI']
    - title: Question
      labels: ['Type: Question']
    - title: Security
      labels: ['Type: Security']
    - title: Dependencies
      labels: ['Type: Dependencies']

    - title: Other Changes
      labels: ['*']

GitHub Release で新しい release のリリースノートを書くところにあるボタンを押すだけ。

youtu.be

これだけです。簡単!

細かい編集をしてから公開も可能

自動生成されたリリースノートはそのまま textarea に挿入されるので、textarea 上で手直しして、それから公開する、ということができるます。結構細かい調整をしたいことがあるので、こういうのができるのはありがたいですね。

youtu.be

グルーピング用のラベルは @azu/github-label-setup を使って用意すると楽

「Type: Dependencies」とか「Type: Security」とかは GitHub がデフォルトで用意しているラベルリストの中に入っていないので、自分で作ってやる必要があります。手で作ると結構面倒なので、id:mizdra@azu/github-label-setup というツールを利用して1コマンドで作ってます。

efcl.info

$ # 事前に @azu/github-label-setup をインストールしたり、envchain に GITHUB_ACCESS_TOKEN を保存しておく
$ npm i -g @azu/github-label-setup
$ ehvchain --set github GITHUB_ACCESS_TOKEN

$ # リポジトリルートへ移動
$ cd mizdra/eslint-interactive
$ # https://github.com/azu/github-label-setup#default-labels にあるラベルリストに変更
$ envchain github github-label-setup -A

オススメです。

最後に

Maintainer Month に合わせて色々な人がオープンソースのメンテナンスについて記事を書かれているので、是非読んでみてください。また普段オープンソースのメンテナンスをしている人は、なにか記事を書いてみて、普段の活動を振り返ったりすると、良い機会になると思います。

efcl.info

sosukesuzuki.dev

極端なパターンを考えて、技術的な落とし所を探っていく

「React コンポーネントってどこまで細かく分割していいの?」とか、「Jest のスナップショットテストテストってどんなテストにも使っていいの?」とか、技術的な落とし所をどうするか悩むことは結構多いと思う。

コンポーネントを分割すると、責務が小さくなったり、再利用しやすくなったり、変更に強くなったり、各種ツールから利用しやすくなったりと、色々なメリットが得られる。けど、だからといってコンポーネント分割のしすぎは良くないと思う。コンポーネント分割するとなると、(そのプロジェクトの規約によるけど) コンポーネントファイルを新しく作らないといけないし、CSS Modules 用の *.module.css ファイルも作らないといけない。ファイルが増えることで、開発中に色んなファイルを行ったり来たりすることになって、作業効率が落ちる場合もある。コンポーネント分割だってタダじゃないし、デメリットもある。だからこそ、どこまでコンポーネント分割していくのか、その落とし所で悩むことになる。

こういう時に落とし所を探す方法はいくつかあるのだけど、id:mizdra はよく「極端なパターンを考える」というのをやっている。例えば「1タグごとにコンポーネント分割していったらどうなるか」とか。

function H2(props) {
  return <h2 {...props} />
}
function Img(props) {
  return <img {...props} />
}
function UserProfile({ useName, avatarUrl }) {
  return (
    <H2>{userName}<H2/>
    <Img src={avatarUrl} />
  );
}

見ての通り、コンポーネント分割したのに責務は小さくなっていないし、再利用しやすくなっていないし、変更にも強くない。コンポーネント分割のメリットが全く享受できていない。けどデメリットは被っている。「ああ、多分細かく分割しすぎたのだな」「メリットが享受できないパターンは NG なのだな」「ということは、責務が小さくなったり、再利用しやすくなると嬉しい場面で使っていくのが良いのかな?」「ロジックが沢山あるようなコンポーネントだったら責務小さくなって嬉しそうだし、色々なページで何回も出てくるようなパーツだったら再利用の効果が効いて良さそうだな」。そういった気づきが得られるはず。

あまりにも愚直なテクニックなのだけど、意外なことに、そこから芋づるのように技術的な落とし所のヒントが見つかってくる。色々な場面で使えるし、バカっぽいアイデアを考えるのが (そして信じられないことに、そのバカっぽいアイデアから気づきが得られるのが) 何より楽しい。オススメです。

実践例

以下の記事も今回紹介したテクニックを使って書きました。

www.mizdra.net

  • 出力の差分が巨大になる場合、スナップショットテストは向いてません
    • 例: 2 京行の差分が出る時
    • 例: Visual Regression Test で、5GB の画像が出力される時
    • テストフレームワークが処理するのに時間が掛かるし、最悪の場合クラッシュします
  • 出力の差分を見て、その妥当性を判断するのが困難な場合、スナップショットテストは向いていません
    • 例: 5 万行の差分が出るテスト
    • 1 分で 50 行見たとしても見終わるのに 1000 分掛かります
      • 9 時に出社して差分を見始めて、お昼過ぎてもまだまだで、定時過ぎてもまだまだで、終電が無くなった頃にようやく見終わる、みたいなスケール
      • 機械より人間が頑張るスタイル
    • ただし差分の内容を見ずに変わったことだけ分かれば良い場合や、最初の 100 行だけ見て妥当ですねと判断できる場合であればこの限りではないです
  • 出力の変化が激しい場合
    • 例: 現在時刻を表示するクロックコンポーネントのテスト
      • テストを実行する時刻によって出力が変わって毎回テストが fail します
      • 毎回人の目でチェックしないといけません
      • 機械より人間が頑張るスタイル 2
      • ただし何らかの手段で出力を固定できれば、問題を回避できる場合があります
        • クロックコンポーネントの例では時間を司る API をモックして時刻を固定すれば良い
        • Visual Regression Test なら変化する部分を黒塗りしてから比較すれば良い
        • このような出力の固定化が困難な場合はスナップショットテスト以外のテスト手法を検討しましょう
    • 例: アプリケーションのソースコードそのもののスナップショットテスト
      • ソースコードを 1 文字書き換えたらテストが fail する、みたいな
      • 誰もそんなテスト書かないと思いますが…
      • 良い具体例が思いつかなかった!本当はちょっとどこかを弄ったら毎回 fail するのは良くないよね、みたいなことを言いたかった。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.