はじめに
佐藤:本日のモデレーターを務めます佐藤歩です。本イベントを主催する株式会社overflowでVPoE(Vice President of Engineer)として開発組織のマネジメントに携わっていますが、ここひと月ほどは久々にフロントエンド開発を行っています。今回のテーマはテストですが、私自身はコンポーネントからロジックを追い出して、容易に書けるユニットテストだけで逃げ切ろうとしている状態なので、今日のtakepepeさんのお話を聞いてこの後の身の振り方を考えたいと思っています。ではtakepepeさん、まずは自己紹介をお願いします。
takepepe:takepepeと申します。職場ではフロントエンド開発の横断的なサポートに携わっています。そして業務外では、『フロントエンド開発のためのテスト入門』を2023年に出版するなど、フロントエンド開発に関連するトピックの発信を普段から行っている者です。本日は皆さんよろしくお願いします。
佐藤:ありがとうございます。それでは早速、takepepeさんから発表をよろしくお願いします。
takepepe:はい。よろしくお願いします。今回は「フロントエンドの書くべきだったテスト書かなくてよかったテスト」というテーマでお話します。まずフロントエンドテストの目的を明らかにした上で、本題である、書くべきだったテストと書かなくてよかったテストについてお話できればと思います。
フロントエンドテストは何のためにする?
takepepe:テストを書く上では、まず目的を明確にすることが重要です。よく社内でも「どうやってテストを書けばいいか」、「どんな観点で書けばいいか」と相談されることが多いですが、そうしたこと以上に「このテストがなぜ必要なのか」をチーム内で議論できているかが大切だと思います。
そして書き始めて書き方がわかってくると「こうやって書くんだな」と理解もしやすいし、テストがパスして気持ちいい感覚を得られるようになり、楽しくなってきます。
その時期を過ぎると、「書きすぎていないか」、「書いたのはいいものの、何に効果があるのかわからない」、「これで本当に適切なのか」などの悩みが出てきます。
テストに期待すること、テストを書く究極的な目的
ここがポイントで、プロダクトと同様にテストでも何か画一的な解があるわけではなく、それぞれの状況によって解が異なります。テストに期待することは、大きく分けるとこの3点に分けられると思います。
たとえばひとつ目のバグを未然に防ぎたい場合、一体どんなバグが起きるのかをきちんと想像できているか否かで結果が変わってきます。インシデント防止の場合も、なぜそれが起きるのかを把握しておく必要があります。
また品質に関しても、品質が高いとはどんなことかを定義しておかなくてはなりません。この3つの点の解像度をどれだけ高くできるかが、テストがうまく機能するかどうかの分岐点になるのではないでしょうか。
こうして根拠のある自動テストを書けると、自信と安心が持てるようになります。テストを書く究極的な目的はこれを得ることではないかと思います。
書くべきテストとはどんなもの?
では前提を共有できたところで、どんなテストがいいものなのかをお話ししていきます。
我々は普段開発する上で、SPAフレームワークやWeb Appフレームワークを使用します。これらのフレームワークにつきものなのが、Routingに関する処理ですよね。リンクコンポーネントボタンやリンクを押すとどこに遷移するのかは、特にテストを重点的に行いたいポイントではないでしょうか。
たとえば、検索クエリです。
ここでfooという値を参照しようとした場合、「string | string[] | undefined 」という型推論になります。そしてUI操作では基本的に「 ?foo=bar(string)」にしかならないよう実装がされているはずです。
しかし実際には、URLバーに「?foo=bar&foo=baz」という異常な入力ができてしまいます。そしてこれをas stringで塗りつぶしてしまうと、このメソッドを要求したときにランタイムエラーになってしまいます。こうした細かいところを見ていくと、稀なケースにも注意が向きます。これを処理するためには、3つの案があるかと思います。
どれを選んでも正解なのですが、どうなってほしいかに応じて何を選ぶかを決めます。これを決める際、エンジニアだけで判断せず、要件定義をしている方に確認を取った上で決めてテストコードを書いていくと、実装の精度も上がり、自信を持ってプロダクトをリリースできることにつながります。
インターフェースに関するもうひとつの事例がこちらです。
操作Aでは「/?foo=bar」を維持して、操作Bでは「/?foo=bar」は除外して遷移するという仕様ですが、操作Cを追加する際にうっかり共通処理としてまとめてしまって、すべて除外してしまうといったミスが起こりがちです。
操作Cの追加により、操作Aにリグレッションが発生しているわけですね。しかし操作Aの自動テストがしっかり書かれていると、こうしたリグレッションを防ぐことができます。自動テストのおかげで助かったという事例ですね。こうした瞬間は、やっぱり「テストを書いててよかったな」と思える瞬間ですね。
続いて、要件が複雑な機能のテストについてです。
実装が完了しテストまで終了していたタイミングで、コンポーネントDに仕様変更が発生したとします。コンポーネントDだけでなく、処理をたどっていくと関数Aと関数Bも修正しなければならなくなりました。そうして修正を行ってコンポーネントDの仕様変更は完了したものの、今度は関数Cがリグレッションしてしまいました。
細かくテストを書きながら開発をすることで、こうした事態は防ぐことができます。そして後戻りが少なく最終的にかかる時間が減り、結果的に早く実装を終えられます。自分自身な開発速度が上がるのはもちろんですが、後任の開発者が安心して機能の追加や変更ができるというメリットもあります。
反対に書かなくてよかったテストは?
では続いて反対に、書かなくてよかったテストについてお話します。
まず、テストサイズが不適切なものですね。
たとえばブラウザの自動テスト。この不適切なサイズとは、他のテスト手法で担保できるテスト観点が含まれていることに気づかず、ブラウザの自動テストを利用してしまっていることを指します。
ほかのテスト手法を知っていれば、適したテストを使ってテストサイズを少しでも小さくできるので、さまざまなテスト手法の得手不得手を把握しておくのはおすすめです。信頼できるテストとは、安定して動いて、かつ忠実性が高いものです。その両方をできるだけ最大化できるよう、それぞれのテストを拡充していけばバランスを取りやすいのではないでしょうか。
ふたつ目の書かなくてよかったテストは、書かれること自体が目的になってしまったテストです。目的や書くべき根拠がないのであれば書かなくてもいいのではないかと思います。
3つ目は無理にaria属性を付け足したテストですね。
比較的有名な格言にも「No ARIA is better than Bad ARIA」とあるように、正しくないaria属性を付与するくらいなら、そもそも付与しない方がよいです。
最後に書かなくてよかったテストは、過剰なビジュアルリグレッションテストです。
これを防止するには、発生しうるケースをどれだけ想定できてるかが重要です。この解像度が低いと、全部のストーリーを対象にしてしまってビジュアルリグレッションテストが過剰になりがちなんですね。ビジュアルリグレッションテストに対して何を期待するのかを議論できるようになると、とてもよいのではないでしょうか。
ここまで駆け足で話してきましたが、「こうした理由で必要です」と言い切れるテストが書いてよかったテストになるのだと思います。ただ、どんなテストでもテストを書くこと自体は基本的にはよいことなので、どんどん書いてどんどん振り返りをして、よりよいテストを皆さんで保守運用していければいいなと思っております。以上になります。
ビジュアルリグレッションテストが必要な場面は、どんな場面?
佐藤:ありがとうございました。自分の手元のコードベースをどんなテスト設計にするかを考え直す良い機会となりました。続いて、あらかじめ用意させていただいたテーマに沿ってお話を伺っていければと思っております。
takepepe:よろしくお願いします。
佐藤:では早速ひとつ目のテーマでお伺いします。ビジュアルリグレッションテストが必要な場面は、どんな場面でしょうか。
takepepe:ひとつはコンポーネントそのものに閉じたテストですね。ただそれ以前に、CSSはいろいろなところから影響を受けるものです。よく使われるNext.jsだと、グローバルのCSSが設定されていて、その上にコンポーネントがあることが多いと思います。そしてたとえばリセットのCSSをリファクターをしたい場合に、それをいじるのってかなり勇気がいりますよね。こうしたリセットCSSをいじりたいし、全体的にビジュアルリグレッションをやりたいときに、ビジュアルリグレッションテストを思い出していただくといいのではないでしょうか。
テストと聞くと、どうしても自動テストを思い浮かべることが多いと思いますが、手元で簡単に回すためのビジュアルリグレッションテストという選択肢もあるんですよね。必ずしもテスト=自動テストではないことを覚えていただけると、より効率的なテストが行えるようになると思います。
佐藤:ありがとうございます。実際ビジュアルリグレッションテストをCIに入れようとすると莫大な時間がかかるという話もありますしね。
takepepe:先ほどお伝えした通り、たまにビジュアルリグレッションテストを活用したい場面があっても、CIの時間がネックであることはわりとあるんですよね。とはいえ、共通コンポーネントを使えるようになるなど、自動テストのビジュアルリグレッションテストを入れるメリットももちろんあります。なのでやはり、開発のタイミングで何を使えば何が起こりうるのかをきちんと議論していくことが重要ですね。
佐藤:ちなみにビジュアルリグレッションテストを適用する範囲は、たとえばそのコンポーネントごとに取るだとか、ページ全体で取るだとかいろいろあるとは思いますが、takepepeさんはどのように選択されていますか。
takepepe:依存関係にあるかどうかが大きなポイントになります。たとえば、小さいUIのパーツで使った大きめのコンポーネントです。やはり小さなパーツを修正したときにどれだけ影響が出るかをすぐ確かめられる場所なので、ビジュアルリグレッションテストの対象としてかなり適切な箇所だと思いますね。
ビジュアルリグレッションテストが不要であるケースについて
佐藤:ありがとうございます。では今の話と関連して、次のテーマであるビジュアルリグレッションテストが不要であるケースについてお話いただけますか。
takepepe:ビジュアルリグレッションテストにおいて一番大変なのは、実はツールチェーンをアップデートした際に壊れがちなところなんです。それを天秤にかけながら実施を検討する必要があります。なので不要なケースというのも、それぞれのプロジェクトの状況によるところが大きくて、一概には言えないのが正直なところです。何もやらないよりも、ちょっとやってみてやっぱいらなかったなっていう判断をするのは、個人的には全然アリだと思います。
佐藤:takepepeさんは新しいプロジェクトを作る際、テストコードもガッツリ残していかれるんですか。
takepepe:テストコード自体を残すこともあれば、そもそものどうやって書けばいいですかといった指針を残すこともあります。テストを書くためのモジュールを整えてから次のプロジェクトに移っています。
佐藤:素晴らしいですね。すべての新規開発の人がそれぐらい丁寧にやってくれると、救われる人が多そうです。
takepepe:そういった環境があると、やはり書く習慣もつくしメリットも見えてくるのかなと思いますね。
「コンポーネントテストが必要な場面」について
佐藤:スタンダードな基準としてそうしたものが用意されていると、チームメンバーの学習効果もありそうですね。ビジュアルリグレッションテストは基本的に適用範囲が線引きになってきますが、コンポーネントテストの場合、コンポーネントの何をどれぐらいカバーしてテストするのかという問題があるように感じます。続いてのテーマである、「コンポーネントテストが必要な場面」についてもお話いただけますか。
takepepe:コンポーネントに関しては、外側からテストができるんですよね。関数や依存する小さいコンポーネントを結合して、それに対して外側から機能的な部分を見ていくんです。こうすることで、小さなコンポーネントや小さな関数にテストがいらなくなります。網羅的にテストが担保できるわけですね。こうしてコンポーネントテストを書くことで、広い範囲をカバーできることがあります。
逆に必要じゃない場面は、ビジュアルリグレッションテストをしっかり行っていて、コンポーネントのインタラクションテストが担う領域がビジュアルリグレッションテストでカバーできているときですね。
ビジュアルリグレッションテストが自動テストに組み込まれている場合、すでに書かれていることをコンポーネントテストでまた書いてしまうことになります。すると先ほど説明したような過剰なテストになってしまいます。
なので、ビジュアルリグレッションテストがあるのであれば、コンポーネントテストはそんなに書く必要はないのかなと思います。逆にビジュアルリグレッションテストをしない場合は、コンポーネントテストをしっかり書いておくといいですね。
どのフェーズからテストを書くべきか、そして何から書くべきか
佐藤:ありがとうございます。では続いて、テーマとしては最後ですね。「どのフェーズからテストを書くべきか、そして何から書くべきか」を伺えればと思います。
takepepe:これもテストサイズによりけりですが、特にビジュアルリグレッションテストの場合、最終的に開発が出来上がった後ぐらいが導入時期としてはベストですね。最初のうちはコンポーネントを頻繁に書き換えたり、新しいpropsを追加したりといったことが起きがちです。そんなとき、ビジュアルリグレッションテスト自体は後から、ただストーリーブックは最初から入れておくことはあります。
また、先ほど複雑な機能はひとつずつ関数を書いていって、スピードに貢献するテストにしましょうとお話しました。このケースでは、機能の実装と一緒に書くべきですね。
このようにテストを書くフェーズはテストサイズと紐づいている部分が大きいので、小さなテストはコーディングと一緒に書いて、大きなテストはリリース前などに着手するといいのかなと思います。