こんにちは。カミナシにて業務委託としてフロントエンドを担当している田村(@junkboy0315)です。皆さんはフロントエンドのテスト、どのように取り組んでいますか?フロントのテストはなかなか難しいですよね。
バックエンドのテストには、「入力、出力、永続化されたデータ」の3つを検証するという基本セオリーがあります。しかし、フロントエンドのテストは、その粒度や手法が多様で、とっつきにくいと感じる方も多いのではないでしょうか。
カミナシでもフロントエンドのテストは以前は十分とは言えない状態でしたが、これまで継続的に改善を重ねてきました。今回は、その変遷についてお話ししようと思います。
夜明け前
カミナシのコードベースでは、元々ユニットテストがある程度整備されていました。これらは主に複雑な計算処理を行い結果を返す関数などに対して実施されていました。
しかし、画面全体の機能を網羅する包括的なテストは不足していました。この結果、リリース後にリグレッションやバグが発生し、ロールバックが必要となることがしばしば発生していました。
この状況を改善すべく、まずは2023年時点で一番イケてるフロントエンドのテスト手法とはなんぞや、を問う旅に出ました。
情報収集 - ブログを漁る
まずはお金のかからないネット検索から始めました。多数の日本語および英語のブログ記事をクロールした結果、多くの記事で引用されている記事がありました。それが、RemixやTesting Libraryの作者であるKent C. Dodds氏が書いた「The Testing Trophy and Testing Classifications」という記事でした。
内容をかいつまむと「過多なテストや実装により過ぎたテストはもはや負債である」という思想をベースとし「いい感じの粒度のテストをほどよく書くことで最高のROIを得る」という趣旨のようでした。ここで言う「Return」は開発者の自信で、「Investment」は費やす時間を指しています。
そのために、テストを以下の4つに分類・定義したうえで、主として統合テストに力を入れるのが良いのではないか、という話だと私は読みとりました。(以降、言葉の定義は以下を踏襲します)
- E2Eテスト - モックがほとんどない完全なアプリ
- 統合テスト - ページ単位でのテスト(HTTP通信先はmswでモック)
- 単体テスト - コンポーネント単位でのテスト
- 静的テスト - Linterなどによるテスト
テストと言えばピラミッド、もしくはプロダクトの状況によっては逆ピラミッドにするのがセオリーと教わってきた世代ですが、少なくともフロントエンドのトレンドは少し変わってきているようです。
情報収集- 本を読む
短時間で効率よく情報を得るには、やはり課金に限ります。そこで、おそらくフロントエンドに関するテストについて書かれた国内唯一の専門書と思われる以下を拝読しました。
この書籍の中では、React Testing Libray(以下、RTL)とmswを使った構成や、Playwrightを使った構成などが紹介されています。具体的なコードやTipsも豊富に掲載されており、大いに参考にさせていただきました。
React Testing Librayでの統合テスト
というわけで早速、統合テストを書き始めました。テストの構成としては、RTLでもPlaywrightでも同じことができそうでしたが、ひとまずはスピード面やコードカバレッジが出せるという点を評価し、RTLで書き始めてみました。
テストコードの詳細な書き方は前述の書籍に譲りますが、大枠として以下のようになります。
- HTTP通信先をモックする
- 共通的なラッパーコンポーネントを準備する
- 主にContext関連のセットアップを担当
- 要素の取得手段をコードとして定義する
- インタラクションをコードとして定義する
- 各テストをAAAパターンで書いていく
- Arrange - テスト対象の画面を描写
- Act - クリックなど何らかのインタラクション
- Assert - 画面が望ましい状態に変化したかどうかの確認
誤算
書き始めると、RTLで統合テストを書くことの辛さが見えてきました。なんといっても、画面のテストを画面が見えない状態で書いたり保守するのが辛すぎるのです。
RTLはJSDOMなどを使ってエミュレートされたブラウザ環境でテストを行いますが、これにより開発者は画面の状態を視覚的に確認することができません。
ではどうするかというと、debugというユーティリティ関数を使います。この関数はコンソール上にHTMLタグを出力するだけなので、実際の画面がどう見えているはずなのかは、開発者が人力でこのHTMLをパースして脳内で再生する能力が求められます。小規模なパーツのテストではこの方法は有効かもしれませんが、画面全体のテストとなると、非常に困難です。
<body> <div> <div> <header aria-labelledby=":r0:" class="css-jv3e08-Header" > <h1 class="css-fp5tkc-Header" id=":r0:" > レポート出力 </h1> </header> ...(以下延々とHTMLタグが出力される)
また、最近のフロントエンドテストでは、ボタンなどのテスト対象要素を取得する際に、アクセシビリティをサポートするARIAという仕組みを活用しています。もしブラウザ上でテストを行うのであれば、視覚的に要素を特定し、デベロッパーツールを通じてARIA関連の属性を容易に確認できます。
しかし、RTLではデベロッパーツールを使用できません。そのため、logRolesというユーティリティ関数を使ってARIAに関する情報をコンソールに出力して確認する必要があります。この出力にはARIAのroleとnameが簡潔にリストアップされるだけで、それぞれの要素が画面上のどの位置にあるのかを特定するためには、開発者の想像力が求められます。
-------------------------------------------------- heading: Name "レポート出力": <h1 class="css-fp5tkc-Header" id=":r1:" /> -------------------------------------------------- link: Name "新規でレポート出力": <a href="/reportExports/new" /> -------------------------------------------------- button: Name "新規でレポート出力": <button class="ant-btn ant-btn-primary" type="button" /> -------------------------------------------------- ...(以下、延々と一覧が続く)
加えて、JSDOMの制約により、ホバー時やスクロール時の見た目のテストができなかったり、ブラウザではうまく動くライブラリがなぜかJSDOMでは動かなかったりするといった問題にも悩まされました。
Playwrightへの移行
統合テストをRTLで行うことには限界を感じたので、次にPlaywrightでテストを書いてみることにしました。
テストコードの構成はほとんど同じなので、RTLで書かれたテストをPlaywrightに移植するのにほとんど手間はかかりませんでした。ちなみに、公式のマイグレーションガイドも用意されています。
Playwrightに移行して特に良かった点は、画面を見ながらデバッグができるということです。debugモードを有効にしてテストを実行すると、実際のブラウザに加えてPlaywright Inspectorなるものが起動した状態で、テストを1ステップずつ実行することが可能です。また、ARIAに関する属性を調べたり、要素を取得するためのコードをワンクリックで自動生成することも可能です。
さらに、最近実装されたUIモードを有効にすると、テストケースを網羅的に画面上で確認できたり、タイムトラベルデバッグが可能になったり、全てのネットワークリクエストのログを閲覧できたりと、至れり尽くせりの恩恵を受けることができます。暗闇のコンソールでもがいていた数日前の自分と比べると飛躍的な進化を遂げたといえるでしょう。
さらにカミナシではPlaywrightをreg-suitと組み合わせることでVisual Regression Testを行っています。テストコードの任意の場所でpage.screenshot()と書くだけで、E2Eテストだけでなく見た目のテストがおまけで付いてきます。VRTのためだけに別のコードを書く必要がないので、コンビニで弁当を買ったら無料でお茶がついてきたみたいな気持ちになれます。
もちろん、RTLと比べたときにデメリットもあります。
一つは速度の問題です。一般的に、一部分のパーツだけをテストするRTL等で行うテストと比べて、ブラックボックステストであるPlaywright等は速度の面で劣ると言われています。ただ、カミナシのコードベースにおいては目に見えてテストが遅いということはなく、むしろ速い場合もあるので、そこまで気に留めていないところです。
また、Flaky Testの問題もあります。今のところほぼ安定していますが、稀にCIでのみテストに失敗する場合があります。コードの書き方に原因がある場合もあれば、リソースの不足による場合もあったりして、調査しても原因究明が難しいこともあります。
最後は、テストコードのカバレッジを把握できない問題です。こちらは潔く諦めています。とはいえ、カバレッジをむやみに広げようとすると、開発者の関心が振る舞いでなくコードに移ってしまって詳細のテストに走りがちになったりするので、特に統合テストに関してはカバレッジは出せなくても差し支えないのでは、と思っています。
統合テストを書くようになって変わったこと
統合テストを導入したことにより、以下のような明確かつポジティブな変化がありました。
まず、当たり前ですがバグやリグレッションがないことに自信が持てるようになりました。統合テストが書いてある画面は、むしろ壊す方が難しいといっていいくらいに堅牢です。このため、普段はバックエンドをメインに触っているエンジニアであっても、フロント側のコードを臆することなく自信を持って編集することが可能になりました。
また、ライブラリのアップグレードを迷うことなくできるようになりました。これは当初は予見していなかったので嬉しい誤算でした。該当のライブラリが使われているページに統合テストがきちんと書いてあれば、CIにパスしている限りはDependabotが作成してくれたPRをそのままマージできます。
さらに、些細な見た目の変化に気がつくことができるようになりました。UIライブラリのアップグレードにより、細かすぎて気づかない見た目の修正がしれっと行われていることがありますが、そういった変化も見逃さなくなりました。
残課題
残された課題として以下がありますが、これらについては目下取り組み中のため、また機会があれば記事にできたらいいなと思っています。
- テストを追加していくほど遅くなるCIをなんとかしたい
- → GitHub-hosted larger runnersを活用できるかな?
- APIのFixtureをコスパよく作成/保守したい
- →Ruby界隈で有名なFactory botの親戚で、Fisheryという子がいるみたいだけど活用できるかな?
- Playwrightのコンポーネントレベルでのテストの仕組みがGAになったら試してみたい
まとめ
というわけで、現在のカミナシではPlaywrightとVisual Regression Testingを組み合わせた統合テストにより、画面全体の機能をチェックすることをメインのテスト戦略としています。あわせて、真に必要な箇所にはVitestやReact Testing Libraryなどを使用し、詳細なユニットテストを過不足なく実施するようにしています。
この取り組みを始めてまだ間もないですが、Dodds氏の言うところの"最高のROI"を得られている実感があります。今後も継続的に改善を続けていきたいと思っています。