サバンナ便り ~ソフトウェア開発の荒野を生き抜く~

第8回脆いテスト
~継続的な変更と改善を阻むテストの原因と対策~

本連載の主なテーマは、信頼できる実行結果にできるだけ短い時間でたどり着く自動テスト群の構築です。連載の区切りとして、なぜ自動テストを書いてメンテナンスしていくのか、そしてそれに立ちはだかる「脆いテスト」fragile testについて整理します。

自動テストを書く動機

自動テストを書く動機には、不具合混入を防止する、問題箇所の絞り込みを容易にする、動く仕様書やサンプルになるなどいろいろありますが、最大の動機は、変化を抱擁し、ソフトウェアの成長を持続可能なものにすることだと筆者は考えています。

ソフトウェアを取り巻く世界は変わりました。ソフトウェアは世界を飲み込み、事業と一体化しました。事業を取り巻く市場もエンドユーザーのニーズも刻々と変化する時代においては、より速く、より安全に変化する力が求められます。コードを変更しなければ動き続けることが期待できる時代ではもうなく、決められたものを決められたときまでに破綻なく作ればよい時代でもなくなりました。変えなくてよいソフトウェアを作るのではなく、欲しいときにすぐに望ましい形にできるような、変更しやすく、変化に対応できるソフトウェアが競争力になる時代に変わったのです。ソフトウェア開発は、事業が成長する限り完成も終わりもなくなり、継続的な変更と改善を続けるものになりました。

継続的な変更と改善を続けるうえで鍵となるソフトウェアの品質特性は「保守性」です。保守性は、モジュール性、再利用性、解析性(理解容易性⁠⁠、修正性(変更容易性⁠⁠、試験性(テスト容易性)といった品質副特性によって構成されています。自動テストはそのすべてに関係しますが、特に最後の2つを大きく向上させるものとして、ソフトウェアの保守性を支える柱となります。保守性という名前からは従来「現状維持力」のようなイメージを抱きがちでしたが、現代においては、変化に対する反応速度、改善力、推進力ととらえるのがよいでしょう。

自動テストが整備されていれば、開発しているシステムが自分たちの想定どおりに動いていることが短い時間で確認でき、誰のコードでも同じように編集でき、動かなくなったらすぐにわかります。すると、いつでもどこからでも改善に着手できるという自信が生まれます。ソフトウェアを取り巻く環境の変化に合わせて迅速かつ安全に変化させていくための根拠ある自信こそが、自動テストの最大の効果だと筆者は考えています。

脆いテスト

では、世の中を見回してみるとどうでしょう。自動テスト、特に自動化されたユニットテストに関する意見や感想で、現在でもよく見かけるものがあります。⁠設計が変わりやすいので自動テストを書くとコストパフォーマンスが悪い」⁠自動テストの修正コストが大きい⁠⁠。これらはソフトウェアに変更が入るたびに自動テストをメンテナンスしなければならない負担を嘆く言葉です。また、それらと表裏一体の関係にある「設計が固まらないと自動テストは書けない」という意見もよく目にします。これらの言葉は、自動テストが変化を支える存在になっていない現状を表しています。ソフトウェアを迅速かつ安全に変化させていくための自動テストが、実際にはむしろ変化の邪魔になっている現状がありそうです。

ソフトウェアを少し変化させるたびに大量のテストを変更しなければならない状況は「脆いテスト」と呼ばれています。脆いテストは、自動テスト群の信頼性を損なわせる偽陽性の一つです。偽陽性とは、プロダクトコードが正しいにもかかわらずテストが失敗してしまう誤検知のような状況であり、開発者が次第にテストの失敗に対して鈍感になっていく原因となります。連載の第2回では、偽陽性のパターンとして「信頼不能テスト」flaky test「脆いテスト」の2つを紹介しました。この2つは紛らわしいですが、テストの実行が不安定で、コードに一切手を触れていないにもかかわらずテストが成功したり失敗したりするのが信頼不能テストで、コードに手を触れるとすぐにテストが失敗してしまうのが脆いテストと覚えてください[1]

脆いテストが多いと、テスト対象の些細な変更であってもすぐにテストが失敗してしまうのでメンテナンスコストがかさみます。すると、自動テストの支えによって変化に対して前向きになるべきところが、テストのメンテナンスが面倒だから変化は面倒だ、やめておこうといったように、むしろ後ろ向きになってしまいます。これでは本末転倒です。

脆いテストの原因と対策

脆いテストの原因はテストコードとテスト対象との高すぎる構造的結合度です。構造的結合度とは、テストコードがテスト対象の構造、たとえば名前空間、クラス、メソッドや関数の名前や型などに依存している度合いです。もちろんテストコードにはある程度の構造的結合はありますが、テスト対象の内部構造やアルゴリズムといった実装の詳細に依存してしまうと、プロダクトコードの内部を少し変更しただけでもテストが失敗するようになってしまいます。

では、実装から遠いインテグレーションテストやE2Eend to endテスト、テストサイズで言うならMediumテストやLargeテストだけで開発すればよいかというと、そうではありません。テストサイズの大きいテストは実行が遅く不安定になり、テスト範囲の広いテストは理解容易性や問題の絞り込みに問題を抱えます。それらだけでは、信頼できる実行結果にできるだけ短い時間でたどり着く自動テスト群を構築できません。可能な限り狭いスコープ、小さいサイズで、かつ変更を支えるテストを書く必要があります。
脆いテストになりにくくする指針を3つ挙げます。

  1. 公開API経由でテストする
  2. 構造単位ではなく振る舞い単位でテストする
  3. 相互作用ではなく事後状態をテストする

ⓐは、テストコードからはテスト対象の公開APIのみにアクセスし、公開されていない要素、たとえばプライベートフィールドやプライベートメソッドは直接扱わないようにするということです。公開されていない要素は、公開APIから見た振る舞いを変えない範囲で変更や改善を行われる可能性が常にあります。公開されていない要素を活用したテストが書かれてしまうと、そういった内部の改善を妨げる原因になってしまいます。

ⓑは、テストとテスト対象の構造を対応付けないということです。クラスやメソッド、関数をテストの数や構造と対応させるのではなく、振る舞いや責務に対してテストを対応付けます。テスト対象の1つの関数が3つの仕事を行う場合は3つのテストを書きます。責務をほとんど持たず、ほかのクラスのテストでカバーされているクラスにテストを書く必要はありません。

ⓒは、テスト対象内部のやりとりをテストしないということです。たとえば、テスト対象オブジェクトAがさらに別のオブジェクトBやCに依存している場合、AとB、Cとの相互作用、たとえばメソッド呼び出しの中身や回数をモックオブジェクトなどを活用してテストしたくなりますが、それは諸刃の剣になります。そういった相互作用のテストを書けば書くほど、些細な内部設計の変更に対してもモックを追随させる必要があり、変更コストが上がっていきます。テストでは外部から観た振る舞い、たとえばテスト対象のメソッドの戻り値や、テスト対象のメソッド実行後の事後状態を検証しましょう。

おわりに

構造的結合に注意し、継続的な変更と改善を支えるテストを書きましょう。本連載はここで一区切りです。みなさま、誠にありがとうございました。

おすすめ記事

記事・ニュース一覧