フロントエンドテスト - Kent C Dodds
テストの種類
End to End
極力モックを排除し、実際のブラウザを通してフロントエンドからバックエンドまでを本番に近い状態でテストする。
ユーザーのように振る舞うヘルパーロボットが実際にブラウザ上のボタンをクリックするなど、実際の操作を行う。
ほぼすべての範囲をテストでき最も信頼性が高い反面、テストの作成、実行環境等によるコストが高く、テストの実行スピードも遅いためフィードバックも遅く、テスト自体も壊れやすい。
テスト対象
- アプリケーション全体
テスト範囲外
- (ほぼ)なし
使用ツール
- Cypress / Playwright
インテグレーションテスト
複数のユニットが互いに統合されることをテストする。
React Hooks(Vue Composables) や サードパーティー製のライブラリを含めて極力モックを排除することでより多くの問題を発見できる。
テスト対象
- MSWでネットワークをモックしてページ単位
- 結合された状態のコンポーネント
テスト範囲外
- バックエンドと正しいデータを渡しているか
- エラーに対応しているか
使用ツール
- Testing-Library
- Vitest / Jest
- MSW (ネットワークモック)
ユニットテスト
依存関係がないユニットをテスト、またはテスト用にモックされたユニットをテストする。
フィードバックが早くコストも低いが、依存関係を Mock しており信頼性が低い。
複雑なロジックや、エッジケースをメインにテストする。
テスト対象
- コンポーネント単位
- コンポーネントを描画しない Shallow Mount は行わないことを推奨している。
- React Hook(Vue Composable)単位
- 関数
- クラス
テスト範囲外
- バックエンドと正しいデータを渡しているか
- エラーに対応しているか
- 依存関係の呼び出しが正しいか
ツール
- Jest / Vitest
Static テスト
タイポや型エラーなどのコード上のエラーをキャッチする。
テスト対象
- 全コード
テスト範囲外
- バックエンドと正しいデータを渡しているか
- エラーに対応しているか
- 依存関係の呼び出しが正しいか
- ビジネスロジック
ツール
- ESLint
- Stylelint
- TypeScript の型システム
Kent C Dodds: The Testing Trophy and Testing Classifications
Kent C Dodds: Static vs Unit vs Integration vs E2E Testing for Frontend Apps
テスティングトロフィー
対費用効果で着眼したときにどのテストに比重を置くべきかをトロフィーの面積で表したもの。
上にあるものほど高コストで壊れやすく、失敗の解析、修正の時間がかかるが、多くの範囲をテストでき信頼性が高い。(コスト:テストの作成コスト、テストを実行するためのCI環境など)
E2Eテストは最も信頼性が高いが、テストの作成やテストの実行環境も含めて高コスト。またテスト実行に時間がかかりフィードバックが遅く、エラーの解析にも時間がかかる。
ユニットテストはテストを作成するのもかんたんで、実行速度も早くフィードバックも早い。しかし Mock するため依存関係をテストすることができず、信頼性が低い。
結合テストはその中間。極力 Mock を排除することで多くのケースをカバーすることができ、信頼性が高く、コストもE2Eほどではない。
最も対費用効果が高いため、結合テストに比重をおいて作成することを推奨している。
Kent C Dodds: The Testing Trophy and Testing Classifications
実装詳細のテストを避ける
実装の詳細とは?
- ユーザー(アプリケーションやコードを使うエンドユーザー、デベロッパー)が見みない、使用しない、知らないこと
- 変数名
- メソッド名
- ボタンを押されるとなんのメソッドが呼ばれるかなど
なぜ実装の詳細をテストしてはいけない?
- 偽陰性/偽陽性が発生しやすい
- 偽陰性
- アプリのコードは間違っていないのにエラーが出る
- 正しくリファクタしただけでテストが壊れる
- 偽陽性
- アプリのコードが間違っているのにエラーが出ない
- リファクタを誤ってもテストがエラーが検出されない
- 偽陰性
実装の詳細を避けてテストを行うには?
- ユーザーが使う方法と同じようにテストを行う。
テストがソフトウェアの使用方法に似ているほど、信頼性が高まります。
- エンドユーザー
- 画面上のUIを操作し、結果が画面上に反映されることを確認する
- デベロッパー
- コンポーネントの Props に値を設定し、画面上の値が更新される
- Store の値を dispatch し、画面上の値が更新される
テストユーザーを避ける
コンポーネントが関心を持つべきユーザー
エンドユーザー
- 実際にアプリケーションを使うユーザー
- ブラウザ上に表示されたコントロールを操作し、状態を変更する(UX)
- UXを変更する場合はリリースノート、トレーニング資料を更新するなどが必要になる。
開発者
- コンポーネントを使って開発を行うユーザー
- コンポーネントが公開するAPI(ストア、Propsなど)をもとに操作を行う
- API 変更する場合はコンポーネントの使い方を変える必要がある
内部リファクタリングのようにUXやAPIを変更しなければ追加の更新は不要となり、コンポーネントに閉じた変更となり追加の変更は不要となる。
テストユーザーとは
- エンドユーザー、開発者が使う方法とは別の方法を使ってコンポーネントを使う
- 変数名やメソッド名、ボタンが押されたらどのようなメソッドが呼ばれる、など
テストユーザーに向けたテストを行ってしまうと内部リファクタリングによってテストが壊れたり、エラーを検出できなかったりするのでテストユーザーに向けたテストは行わないようにする= 実装詳細のテストを避ける。
何をテストするべきか
ユースケース
- テスト対象がユーザーにどのように使われるか
- テストは変更が加わってもそのユースケースをサポートしていることを保証するために書く
ユースケースカバレッジ
- どれぐらいのユースケースをテストしているか
- 自動で検出する方法はない
- コードカバレッジが100%でもユースケースカバレッジが100%になるとは限らない
- コードではなくユースケースをテストしよう
テストの優先順位
- テストコードもプロダクトコードも本質的には変わらない
- トレードオフを考えて今優先するべきことを先に行う