🥸

今日からはじめるReactのテスト戦略

2021/11/30に公開

あるフロントエンドの悩み

いざテストを書こうとして色々と調べはじめると、厄介な壁に色々とぶちあたります。

  1. テストの種類多すぎてよくわからない問題。
  2. 何をどうテストすれば良いのかわからない問題。
  3. テストのためのツールどれを選んだらいいか問題。
  4. ...エトセトラエトセトラ......の問題。

今回の記事は「最初はこんな感じで書き始めたらいいんじゃないか」という皮切りになれば幸いです。テストに関する複雑な問題を整理して、よしとりあえずこのルールで書いていこうっ、となるのを着地点とします。

1. テストの種類多すぎてよくわからない問題

テストに関する用語は数多く飛び交っていて非常に混乱させるので、整理します。
登場する用語の定義には、あまり厳密さを求めないでください。
エンドトゥーエンド(E2E)テストをブロードスタックテストと呼んでも、機能テストと呼んでも、あるいは単にUIテストと呼んでも、そこには多少のニュアンスの違いはありますが、だいたい同じことです。そして実際、どこまでが結合テストで、どこからがE2Eテストか、明確にボーターラインが引くことはできません。

テストの分類は個別のバケットというよりもスペクトルであり、単体テストよりの結合テストからE2Eよりの結合テスト...というようなグラーデーションになっています。
定義より大事なのは、チームの中でテストの方針について合意とテストに役立つ共通の語彙を得ることだと思います。

ピラミッド型

テストピラミッドは従来からあるテストの分類で、Mike Cohnという方が書いた著書「Succeeding withAgile」が初出しらしいです。
テストピラミッドでは、下の図のようにテストをUIテスト、サービステスト、単体テストの3つに分類します。
このモデルは、単体テストからより広範なテストさまざまなタイプのテストがあり、ほとんどのテストを単体テストとして実行する必要があることを示しています。

testPyramid

ハニカム型とトロフィー型

ハニカム型とトロフィー型はその詳細は異なりますが、どちらもピラミッド型のテストに対する反応として現れてきたものであり、ピラミッド型の抱える問題を改善する提案である点で共通しています。
ピラミッド型の問題は、単体テストに重きを置いているので、テストを変更せずにコードを変更する方法が制限されることです。リファクタリングする際に同時にテストも変更してしまうと、コードが以前と同様の機能を果たしているという確信が失われます。

ハニカム型とトロフィー型のどちらが良いかという議論は横に置いて、以下ではトロフィー型を採用しながらテストの戦略について話を進めていきます。
ハニカム型についてより深く知りたい方は、shopifyの記事Testing of Microservicesを、トロフィー型についてより深く知りたい方は、Kent C. Dodds氏のThe Testing Trophy And Testing Classificationsをご覧ください。

トロフィー型ではテストを以下のように分類します。

  1. 静的テスト
    コードのタイプミスや型のエラーをチェックするテスト。
  2. 単体テスト
    個々の独立した関数が問題なく動作するかどうかをチェックするテスト。
  3. 結合テスト
    個々の関数が組み合わさってできた機能が問題なく動作するかどうかをチェックするテスト。
  4. E2Eテスト
    テストを担当するチームが人力で行うようなチェックをロボットに代替させるテスト。

2.何をどのくらいテストすれば良いのか問題

ひとまずテストの分類に関して整理できたので、 何をどうテストすれば良いのか問題に入っていきたいと思います。

トロフィー型のテストの分類

説明のためにトロフィー型の分類を順にレベル分けします。

  • 静的テスト - Level.1
  • 単体テスト - Level.2
  • 結合テスト - Level.3
  • E2Eテスト - Level.4

このようにすると、次のような傾向を見出すことができます。

  • レベルが高いほど、実行速度が遅く、メンテナンスに時間がかかる
  • レベルが高いほど、ユーザーが実際にアプリを扱う体験に近い

@testing-libraryの原則に、

テストがソフトウェアの使用方法に似ているほど、信頼性が高まります。

とあるように、ユーザーが実際にアプリを扱う体験に近いテストは、エンジニアにコードへのより多くの自信を与えます。
しかし、トロフィー型のポイントは、このより多くの自信をメリットは実行速度やメンテナンスコストとトレードオフだということです。
もう少し詳しくこのトレードオフについて見ていこうと思います。

トレードオフについて

以下の観点では、一見、実際にユーザーがアプリケーションを使うシナリオに沿って行うE2Eテストが最高のテストのように思えます。

  • エンジニアは、実際に動かしてテストすることにかける時間を減らせる
  • マネージャーは、失敗したテストがユーザーに与える影響を簡単に判断できる
  • テスターは、バグを見逃す心配がなくなる

良いことづくめのように思えます。
しかし、実際はこううまくはいきません。
E2Eテストを中心にテストを書くといかの事態が起こり得ます。

  • E2Eテストが失敗する根本的な原因を見つけるのは苦痛であり、長い時間がかかる
  • E2Eテストの動作がときどき不安定で、実行する度に別の結果が出る
  • テストの作成やメンテナンスに時間がかかり、納期に間に合わない

E2Eは確かに、より多くのバグをキャッチし信頼を与えますが、あくまで最終的に価値を生むのはテストではなくバグのFIXです。
だからE2Eですべてやるのではなく、よりバグの要因が限定しやすいテストであったり、実行速度が早くメンテナンスしやすいテストが欠かせまん。

また、E2Eテストと同じように、単体テストにもメリットとデメリットがあります。
単体テストは非常にスピーディで失敗したときに原因が非常に容易に特定できます。
しかし、タイヤをどんなに入念にチェックしようと脱輪する可能性があるのと同じで、それらが組み合わさったときにうまく機能するかどうかはわかりません。

上記を踏まえると、バランスの取れた結合テストに重きを置くのが良い方法のようです。
(他のテストを書かないということではありません)

テストのバランスに関する問題は、家の壁を塗るのにローラーとブラシどちらが適しているかという問題にも似ています。
すべてをブラシ(単体テスト)で塗ると時間がかかりすぎますが、全てをローラー(E2Eテスト)で塗るとそれは荒く、塗りきれていない箇所が生じます。
まとめると、適材適所というか、うまく使い分けていくのがベストです。

3.テストのためのツールどれを選んだらいいか問題

VueやAngularについて詳しくはないので、ここでは主にReactにおけるテストに使うツールについてご紹介します。

テストランナー/テストフレームワーク/アサーションライブラリ

JavaScriptのテストのツールには以下のようなものがあります。
Karma, Jasmine, Mocha, Chai, Jest
特別なこだわりがなければJestを用いるのが良いと思います。
理由としては、まずFacebook謹製です。それからJestだけで大抵のことができる利便性があること、複雑な設定なしで小さくはじめられることも挙げられます。
特にcreate-react-appを使う場合には、Jestのテスト環境はあらかじめ自動で設定されるので便利ですね。

DOMテスティングライブラリ

Jestだけだと、Reactのコンポーネントをテストすることができないので、DOMのテスティングライブラリが必要です。
DOMテスティングライブラリ以下のようなものがあります。
Enzyme, React Testing Library
特別なこだわりがなければReact Testing Libraryを用いるのが良いと思います。
理由としては、Enzymeはバージョン17以降のReactに対して互換性がないからです。
詳しくはUpdate addons-test-utils.mdのPRをご覧ください。
React公式ドキュメントで推奨されていたテスティングライブラリも、EnzymeからReactTestingLibraryに変わっています。

E2Eライブラリ

E2Eテストも行いたいときには、cypressというライブラリがあります。cypressSeleniumよりテストの実行が速く、公式ドキュメントも整備されていて読みやすいです。
画面をポチポチしてテストを作成できるAutifyというのもあるようですが、試したことはありません。
seyaさんが記事を書かれていました。
Cypress をお供にE2E受け入れテスト駆動開発 〜そしてAutifyへ〜

その他多くの問題

テストの種類多すぎてよくわからない問題とテストのためのツールどれを選んだらいいか問題について書きてきましたが、テストを書き始めると他にも色々と迷ってしまう部分が生じてくるので、以下に補足としてまとめていきます。

テストメソッド、日本語で書くか英語で書くか問題

日本語で書いた方が良いと思います。
英語が堪能な人であっても、日本人なら英語よりも日本語について豊富な語彙を持っていることが多いからです。豊富な語彙を持ってテストメソッドが書かれると、そのテストが何を意図して何をテストするものなのかについて誤解が生じにく位はずです。
日本語テストメソッドについてどう思いますか?
というスライドを読んだのですが、やはり日本語の方がメリットが大きそうな気がしました。

実装の詳細テストをするかどうか問題

実装の詳細テストとは、ユーザーが知ることのないことをテストすることです。
例えば、ユーザーはボタンを押すとモーダルが開くということについては知っていますが、モーダルが開いているときにはisDialogOpenのstateの値がtrueだということは決して知りえません。
実装の詳細テストは非常に壊れやすいので、基本的には避けるべきです。
ボタンを押すとisDialogOpenの値がtrueになることよりも、ボタンを押すとモーダルを開くことをテストした方がより多くの自信を得られると思います。

テストをネストするかどうか問題

基本的には、

describe("パスワードのテスト", () => {
  test("パスワードが8文字未満だと、エラーが表示される", () => {}
)

のようにネストするより、

test("パスワードが8文字未満だと、エラーが表示される", () => {})

のようにネストしないで平たく書くて、一つのテストメソッドの中に複数のアサーションを含めるのをOKとするのが良いと思います。
テストの量が多くなったり複雑化するとネストしているテストは読みにくくなってくるからです。
ライブラリの性能も上がっているので、テストメソッドとアサーションを1対1対応させなくても、テストが失敗した箇所は十分に一目で特定できるようになっています。

まとめ

長くなってしまったので実践部分を別の記事に分けたいと思います。
実践部分は今日からはじめるReactのテスト実践に書きました。
最後までお読みいただきありがとうございました!

参考

Static vs unit vs integration vs E2E tests
The testing trophy and testing classifications
On the diverse and fantastical shapes of testing

GitHubで編集を提案

Discussion