BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル シンプルなテストピラミッドの提案 ~ テストを有効活用するためのイロハ

シンプルなテストピラミッドの提案 ~ テストを有効活用するためのイロハ

キーポイント

  • テストの効果は、テストを書くコストや、実行コスト、維持コストを上回るべきである。
  • 遅いテストは、時間の経過とともに、テストスイートのコストの最大要因の1つになる傾向がある。
  • テストスイートの期間を少し短縮するだけでも時間の節約に貢献する。
  • テストコードのリファクタリングは、プロダクションコードのリファクタリングと同じレベルで注意すべきである。
  • 時間がかかりすぎるテストスイートは実行時にビルドを失敗させるようにする。

テストラベル

開発者は自動テストを書くためにさまざまなラベルを使っている。例えば単体、統合、受け入れ、コンポーネント、サービス、E2E、UI、データベース、システム、機能、または APIなどなど。これらのラベルにはそれぞれ異なる意味があり、テストのスコープ、アクションの種類、テスト対象、またはテスト対象のコラボレーターのいずれかを表している。私たちはたいていラベルのそれぞれの意味について共通言語化しておらず、定義に関する無駄な議論を延々と繰り広げてしまうこともしばしばである。

私は、どのラベルを使うか、どのように定義するかを議論するよりも、それぞれのテストに単純に「遅い」「速い」というラベルをつける方が役に立つと考えている。この考え方はテストスイートの構成を決めるときにも有効で、開発者は非生産的な議論をせずとも、客観的にテストを分類できるからだ。

テストラベルの選択はテストスイートの構成に重要な影響を及ぼす。開発者は与えられたふるまいに対して、いつテストを書くべきか、どのタイプのテストを書くべきか、そしてテストスイート全体のバランスを評価するためにテストラベルを使用する。これを誤ると、正確ではないカバレッジのテストスイート、もしくはカバレッジは担保するが受け入れがたいコストのテストスイートを作ってしまうことに成りかねないのである。

テストを書くべき時

プロダクションコードに対して、いつテストを書くべきだろうか?私のようにエクストリームプログラミング(XP)やテスト駆動開発(TDD)を実践している開発者は、この問いに対して「常に(Always)」と答えがちである。しかしながら、すべてのコードのテストは自動化するべきではないし、それぞれのテストについて、まずテストの作成コストとメリットを比較して進めるべきなのである。

私はテストを書くことに反対しているわけではない。実際ほとんどのテストはこのチェックの答えが「はい」になるだろう。このチェックは実行に時間がかかるテストや、書くのに時間がかかるテスト、あるいはメンテナンスが難しいテストに有効である。試しに自分自身にいくつかの質問を投げてみると良い。

Q そのテストにコストを要するのは、設計上の判断によるものなのか?
Q テストのためにコードをリファクタリングすることは可能か?

テストはプロダクションコードの最初の利用者である。コードをテストしやすくすることで、コードを利用しやすくなり、コードの品質が向上することは頻繁に起こることだ。

Q そのテストは、テスト手法のせいでコストを要するのか?
Q 別のテストアプローチならば、もっと簡単に書けるだろうか?

コラボレーターの代わりに、フェイクやモックのようなテストダブルを使うことを検討する。テストに複雑な設定が必要な場合は、これをテストシナリオに抽出しテスト間で再利用できるようにする。

テストダブルの過度な利用に注意すべきである。理由は本物のコラボレーターほどの信頼性は得られないからだ。たしかにフェイクやモックを用いたテストは、セットアップの容易さ、テスト期間の短縮、個々のテストの信頼性の向上といった観点で見合う場合もある。しかしテストダブルに頼りすぎるとテストと実装が一体化してしまい、全体としてリファクタリングを阻害する信頼性の低いテストスイートになる可能性があるのだ。

Q 本質的にテストが難しいためにテストのコストを要するのか?

こういった場合はテストしている機能の重要性を考慮する。例えば、支払い処理に関わる重要な機能であれば、かかるコストは妥当かもしれないしディスプレイロジックのエッジケースならば、テストが本当に必要であるかどうか再考すべきである。

Q 予測できない失敗をするテストにコストを要していないだろうか?

もしそうならばテストを削除するか、信頼性の高いテストに書き直すか、テストスイートを分離すべきだ。テストスイートが有用なフィードバックを提供するためには、テストの失敗が望ましくない動作であることを確信するべきだ。予測ができないが必要なテストならば、実行頻度の低い別のテストスイートに移動すべきだろう。

テストピラミッド

開発者はいつテストを書くか、そしてどのような種類のテストを書くかを決めるのに、テストのラベルをテストピラミッドに置いて特定のテストタイプの重要度を伝えることがある。

テストを説明するために使われるラベルの種類が多いため、テストピラミッドの形はケースバイケースで少しずつ異なる。「テストピラミッド」で画像検索してみると、最初のページでは数個のピラミッドが似た結果となるだろう。それぞれのピラミッドは一般的に一番下に低コストの単体テスト、一番上に高コストのシステムテスト、そして真ん中に中くらいのコストのテストが何層にも重なっている。

テストピラミッドを活用する前に、チームはテストピラミッドにどのラベルを含めるか、それぞれのラベルの定義は何か、ピラミッドにどのような順序でラベルを含めるかを決定するべきである。なぜなら開発者は独自のテストのためのラベルセットを持つ傾向があり、各ラベルが何を意味するかについてチーム内(またはチーム間)で包括的な合意がないからだ。

ほとんどのテストピラミッドで最下段は単体テストだが、「単体(Unit)」という言葉が何を指すのかについては、残念ながら大きな認識の不一致が存在する。この意見の相違は、テストスイートのコストを削減することよりもラベルに関する議論になりがちであり、テストスイートの有用性が低下してしまうのである。

スピードにこだわる

スピードはテストスイートの運用コストにもっとも寄与する。迅速なフィードバックのために開発者はテストスイートを1時間に複数回実行する必要があるため、スイートの実行時間がわずかに増えるだけでも、結果として長時間の待機時間につながることがあるのだ。

テストの実行待ちは極めて非生産性的な時間である。テストスイートが非常に遅い場合(5分以上かかる場合)、開発者はテストの実行中に他のタスクに取り組むことがよくある。このタスクスイッチは有害であり、集中力を減少させ開発者がコンテキストを失う結果になる。時間のかかるテストスイートが終了した後、開発者は元のタスクを再開する際に復帰のための時間を要するのである。

より良いピラミッド

テストの速度にこだわると、もっとシンプルなテストピラミッドが求められる。

このピラミッドはテストスイートにはできるだけ多くの高速テストと目的のふるまいを完全にカバーするのに十分な低速テストが必要だという明確なメッセージを伝えている。一般的な(そして複雑な)テストピラミッドと同じメッセージだが、開発者が理解し、同意するのは非常に簡単である。

とあるテストを一般的なテストピラミッドのどこに配置するかは、開発者によって意見が分かれるかもしれないが、あるテストがこのピラミッドのどこに当てはまるかは簡単である。チームはどれが速いテストであるか、どれが遅いテストであるか、に合意するだけである。ビジネスドメイン、言語、フレームワークによって閾値は異なるかもしれないが、テストの速度は客観的に測定できるからである。

高速なテストスイート

テストスイートは初期段階では高速だが、その状態を維持することは稀である。時間の経過とともにテストが追加され、開発者は遅いテストスイートに対する許容範囲を広げていく。開発者の多くはテストスイートを高速に保つプロダクトの経験がないため、高速なテストスイートが実現可能であることに気づいていないのだ。

テストスイートを高速に保つには訓練が必要である。開発者はテストスイートを追加するときはいつでも精査し、わずかなテスト時間の短縮でも得られる効果を認識するべきである。例えば6人の開発者からなるチームの1人が、テストを10秒速くするために4時間を費やした場合、その投資はわずか6週間で償却できるのだ(開発者が1日のうち1時間に1回テストを実行すると仮定)。

制限を設ける

チェックを外すと、テストスイートの長さは時間の経過とともに指数関数的に増加する。つまり長さは現在の継続時間に比例して長くなる。テストスイートが10秒で完了するならば、開発者はビルド時間が1秒増えることに悩むかもしれないが、テストスイートに3分かかる場合はそのことに気づかないかもしれないのだ。

テストスイートの実行時間が1分以上かかる場合はビルドを失敗させるなど、指数関数的な増加を防ぐ方法として、テストスイートの実行時間に厳しい制限を設けるべきである。テスト実行に時間がかかりすぎるとビルドは失敗するので、開発者はテストを高速化するために時間をかけなければならなくなる。そして制限時間を長くしてビルドを修正することを許してはいけない。なぜテストが遅いのかどうすれば速くできるのかを理解するために時間をかけて考えてもらうのである。

リファクタリング

テストコードはプロダクションコードと同様の注意と検証が必要である。テストコードを適切に構造化し高速に保つために継続的にリファクタリングを行い、テストスイートの維持と実行のコストを最小にする。テストのリファクタリングではテストコードやプロダクションコードのふるまいを変更してはいけないということを念頭におき、むしろ可読性、保守性、実行速度を向上するように、コードを修正することが求められるのである。

どうしても遅いテストがあるならば、異なるテストスイートに分離する。この遅いテストスイートは、メインのテストスイートのように頻繁に実行するものではなく、追加のカバレッジを提供するために存在するものと考える。ビルドプロセスをブロックしてはいけないが、テスト対象のふるまいが正しく機能していることを、検証するために定期的に実行する。

既存のテストスイート

もし読者のみなさんが現在、別のテストピラミッドを採用していたとしても、アプローチの変更をするには遅すぎるということはない。みなさんが複雑なテストピラミッドに従っているのなら、テストの多くはピラミッドのラベル名を含んでいると考えられるからだ。

最初の一歩として、テスト名を変更する時間をとってもらいたい。新しいテスト名は、テストのラベルではなく、テストするふるまいを反映させるのである。たとえば、UserIntegrationTest を UserAuthenticationTest に、RegistrationApiTest を AddPaidUserTest といった具合である。

このプロセスでは、新しい名前の間でいくつかの衝突が発生する可能性がある。この衝突は同じふるまいを検証する複数のテストが存在するという警告そのものである。重複を解消するために、これらのテストの移動、結合、名前の変更、または削除をじっくりと実施するのである。

テストの名前を変更したら、テストのディレクトリ構造を再編成し、ふるまいに従ってテストをグループ化する。こうやって整理することで、コード上で同時にふるまうテストの関連性を見出して重複するふるまいを検証するテストを発見するのに役立つのである。

遅いテストスイート

遅いテストスイートはすぐに対処するべきである。さらに遅くならないように、すぐにテストスイートの実行時間に制限を設ける。次に、各テストまたはテストグループの実行時間をリストアップして、遅いテストを検知する計測器を追加する。このプロセスで容易に高速化できるテストがいくつか見つかるはずである。

これらを修正すれば改善が難しい遅いテストが残る。高速テストをスイートから分離し、残りの低速テストとは別に実行できるようにする。こうすることで、一部のテストはすぐにスピードアップし、改善のための時間を確保できるのである。

定期的にテストのスピードアップに時間を割くようにする。遅いテストでカバーされているふるまいが、より速いテストでカバーできる(あるいはすでにカバーされている)かどうかを調査するのである。よくある例としては、ブラウザを動かすテストで多くのエッジケースをカバーすることが挙げられる。ブラウザのテストは時間を要するので、もっと高速な低いレベルのテストでその検証を代替できることが多い。

実践にむけて

システムテストと統合テストのどちらを書くか議論するならば、その前に少し考えてもらいたい。その2つの区別が、さほど重要ではないことに気づくのではないだろうか。コストを最小限に抑えながら高い信頼性を得ることが目的であれば、いかに低コストで目的のふるまいをテストできるかを議論することになる。この方向で議論を進めれば、より生産的な結果を得られるのである。

テストのラベルばかりに注目するのではなくもっと重要なことに注目すべきである。テストが遅いのならば速くする。それが難しい場合は、かわりに狭いスコープのテストで同じカバレッジを提供することを試みる。それができないならばテストの効果が、遅いテストに要する多大なコストに見合うかどうかを検討する。その価値があるならば、ビルドを妨げない別のテストスイートに遅いテストを分離することを検討するのである。

新しいテストピラミッドに従って、テストのスピードにこだわって、テストスイートを高速化し、信頼性を高く保つようにしてもらいたい。

作者について

この記事に星をつける

おすすめ度
スタイル

BT