この記事は リクルートライフスタイル Advent Calendar 2018 8日目の記事です。
2018/12/10更新:続編で フロントエンドでTDDを実践する(react-testing-libraryを使った実践編)を書きました。
はじめに
自分のフロントエンドチームでは、TDDでの開発フローを実施することでフロントエンド開発の課題に向き合っていきます。
今回は、一般的に難しいとされるフロントエンドでのテストについて、どんな方針でテストを書けばいいかについて書いてみたいと思います。
フロントエンド開発の課題
プロジェクトによりますが、テストに関連するものでは以下のようなものが挙げられます。
- 実装する仕様について、プロジェクト内でどう認識合わせするか?
- 開発工程のリライアビリティをどう担保するか?
- テストの精度、粒度をどう考えるか?(クロスブラウザ、ユーザーの操作等の副作用、コストメリットなど)
EX. バリデーション付きTextFieldのテストを考える
「ユーザーがテキストフィールドに文字を入力して、バリデーションエラーが表示される」という一連の流れを5つのステップに分けると下記のようになります。
細かく書くなら①〜⑤の各ステップでテストを書くことができますが、全て書いていたらどんなに工数があっても足りないですよね。我々は意味のあるテストを取捨選択して書く必要があります。
ではどういう指針でテストを書けばよいのでしょうか。
指針: The Testing Trophy
via: https://blog.kentcdodds.com/write-tests-not-too-many-mostly-integration-5e8c7fff591c
テスティングトロフィーはテストピラミッドに似た概念ですが、各種テストのコスト高さ(実行速度、開発/保守工数)に加えて、各テストの効果(どれほど大きな問題を解決できるか)を示した図です。
上に行けば行くほど解決できる問題が大きく、横に広ければ広いほどカバーできる範囲が広いことを示しています。
- Static
- FlowやTypeScriptなどの静的型解析を導入することで、タイポや型エラーをチェックする
- Unit
- 単純なコンポーネントや1関数の振る舞いをチェックする
- Integration
- 各コンポーネントや関数、ライブラリを組み合わせたときの振る舞いをチェックする
- End to End
- サーバーのAPIやブラウザといった実際の環境でアプリケーションを動かして、シナリオ通りに動くかをチェックする
フロントエンド特有の課題
Write tests. Not too many. Mostly integration. - Guillermo Rauch
現在のフロントエンドでは、多様なコンポーネントを組み合わせて複雑なアプリケーションを構築していくという前提があります。
unit testを書きすぎない
そのため、コンポーネント単体のUnit Testはテストコードが実装同等になってしまったり、ロジックのないところに書くことになってしまうことがあり注意が必要です。
例えば、「primaryボタンは緑色であること」というテストケースは、実装そのものの(implementation details)のテストになってしまいます。これはまるで、「シロクマは白い」ことを確認するくらい意味がありません。
また同様に、「コンポーネントはComponentDidMountでItemList APIをfetchすること」は、内部実装に依存しすぎていて、仕様そのままにリファクタしても簡単にテストが壊れてしまいます。
より多くのIntegration testを書く
様々なコンポーネントが複雑に絡み合うフロントエンドにおいては、ユニットテストよりもインテグレーションテストのほうがより解決する範囲が広くなることが多いです。
例えば、「コンポーネントを表示すると、APIの返り値の数だけItemListが表示される」というテストはビジネスロジックを的確にテストできます。
テスティングトロフィー考案者のKent C Doddsは、トロフィー図の比重を意識するとコストメリットのバランスが良くなるとしています。
まとめると、ロジックを持った単一モジュールにはユニットテストを書き、それ以外の機能仕様についてはインテグレーションテストでカバーする という方針が良さそうです。
EX. 改めてバリデーション付きTextFieldのテストを考える
テスティングトロフィーの概念を踏まえた上で、冒頭の例を見てみましょう。
Unit Test
①、③、⑤についてはロジックを持った単一モジュールと言えるので、ユニットテストを書く対象となります。特に③については書いておくべきです。
一方で②、④についてはロジックというよりも実装そのものであり、ユニットテストを書く対象外となります。
Integration Test
図の①→⑤の一連の流れが、フィールドに半角数字以外の文字列を入力すると「半角数字で入力してください」というメッセージが出る というインテグレーションテストです。
Congratulations! これでどのテストを書けばいいかが明確になりましたね。
後者のインテグレーションテストはまさにアプリケーションの機能仕様そのものです。つまりIntegration Testの1ケースはアプリケーションの機能仕様と対となり、各Integration Testのケースを束ねるとアプリケーションの機能仕様ドキュメントにもなります。大規模開発の場合はプロジェクト内でエビデンスとして共有することもできます。
続編:Integration Testをどう書くか?
さて、テスティングトロフィーの指針に則り、テストの方針が整いましたがここで重要なテーマが残っています。「どうやってコンポーネントのテストを書くか?」です。
自分のフロントエンドチームでは、react-testing-libraryとうい新しいテストライブラリを使ってTDDを実践しています。
長くなってしまうので、続編として「react-testing-libraryでTDDを実践する」を別の記事で書きたいと思っていますが、簡単に触れておきます。
react-testing-library
テスティングトロフィーの提唱者として本記事で触れたKent C Doddsがenzymeのリプレイスを意図して作った、Reactのための新しいテストユーティリティです。
設計思想として以下のようなものを掲げています:
The more your tests resemble the way your software is used, the more confidence they can give you
一言で言うと、ユーザーが実際に使うようにテストされているべきだという話で、react-testing-libraryの提供するAPIを使うと、ユーザーがアプリケーションを操作するのを模倣するようにテストコードを書くことができます。
詳しくは続編の記事で紹介したいと思います★
まとめ
フロントエンドのテストは何をどこまで書くか分かりづらいことが多いですが、従来のテストピラミッドに加えて、テスティングトロフィーの概念を元に整理すると書くべきテストが見えてきます。
個人的には、 ロジックを持った単一モジュールにはユニットテストを書き、それ以外の機能仕様についてはインテグレーションテストでカバーする という方針をおすすめします。
今回は簡単な例で紹介しましたが、これはより複雑な機能についても適用できるものなので、ぜひ実践してみてください。