コードカバレッジとユニットテスト自動生成の落とし穴

(この記事は、開発元Parasoft社 Blog 「Code Coverage and the Pitfalls of Automated Unit Test Generation」2018年3月16日の翻訳記事です。)

先日、コードカバレッジの数字だけを追求すると罠に陥りやすいという記事を書いたところ、反響が大きかったので、今回はコードカバレッジの問題と解決策について、もっと掘り下げてみようと思います。具体的には、カバレッジの数値自体について、自動生成のユニットテストの価値について、問題のあるユニットテストの識別方法についてです。 また、カバレッジの計測をうまく行っていく方法についても説明します。

カバレッジの計測

ではまず、カバレッジメトリクスそのものから始めましょう。コードカバレッジの値自体は無意味であることが多い、というのは言い過ぎかもしれませんが、どんなによく言っても誤解を招きやすいものであるのは確かです。もし、コードカバレッジが100%だったとして、それはどういう意味を持つのでしょうか?そのカバレッジはどのように測定されたのでしょうか?

カバレッジを測定するにはさまざまな方法があります。

コードカバレッジを測定する1つの方法は、要件の観点からです。すべての要件に、それぞれに対応するテストがありますか?これはスタート地点としては妥当です……しかし、すべての要件にテストが存在するからといって、すべてのコードがテストされたわけではありません。

コードカバレッジを測定するもう1つの方法は、合格したテストの数によるものです(笑ってはいけません、私は現実にこういう方法を耳にしています)。本当に、冗談ではないのです。これはかなりひどい指標であり、明らかに無意味です。単純にテストの数を数えるのと、どちらがましでしょうか?私にはなんとも言えません。

ここでやっと、どのコードが実行されたかを判別する方法の話になります。一般的なカバレッジメトリクスには、ステートメントカバレッジ、行カバレッジ、ブランチカバレッジ、デシジョンカバレッジ、複数条件カバレッジ、またはより包括的なMC / DC(Modified Condition / Decision Coverage)などがあります。

もちろん、最も簡単な方法は行カバレッジですが、おそらくこれをお読みの方にも経験がおありかと思いますが、ツールによって計測方法が異なるため、レポートされるカバレッジ結果もさまざまに異なります。また、コード行が実行されても、そのコード行で発生する可能性のあるさまざまな事象がすべてチェックされたわけではありません。そのため、自動車の機能安全のためのISO 26262や航空機搭載システムのためのDO-178B / Cなど、セーフティクリティカルなシステムのための規格は、MC / DCを要求しています。

下に簡単なコード例を示します。x、y、zはブール値であるとします。

If ( (x||y) && z) { doSomethingGood(); } else {doSomethingElse();}

この場合、どのような値で実行しても、この行は「カバーされた」行になります。確かに、こんなふうにすべてを1行にまとめるのは、良いコーディングではありませんが、問題の要点はおわかりかと思います。そして実際にこのようなコードを書く人もいるのです。コード例を少しきれいにしてみましょう。

If ( (x||y) && z) {
        doSomethingGood();
} else {
        doSomethingElse(); /* because code should never doSomethingBad() */
}

ぱっと見では、2つのテストがあれば十分だという結論に至るかもしれません。つまり、式全体をTRUEと評価し、doSomethingGood()を実行するテスト(x = true、y = true、z = true)と、式全体をFALSEと評価してdoSomethingElse()を実行するテスト(x = false、y = false、z = false)です。行カバレッジでは「すべてがテストされた」となり、問題はありません。

でも、ちょっと待ってください。式全体をテストする方法はほかにもあります:

xの値 yの値 zの値 デシジョンの値
False False True False
False True True True
False True False False
True False True True

 

これは簡単な例ですが、問題をわかりやすく説明しています。MC / DCカバレッジを考慮するなら、コードを適切にカバーするには4つのテストが必要です。行カバレッジなら、この半分の時点で100%となったでしょう。MC / DCの価値についての詳しい説明は、別の機会に譲りたいと思います。ここでのポイントは、どのような方法でカバレッジを測定するにしても、アサーションによって何を検証するのかが意味を持つということです。

無意味な自動生成

多くの人が陥るもう1つの罠は、簡易的なツールを使用してユニットテストを自動的に生成することです。

シンプルなテスト生成ツールは、アサーションなしでコードを実行するテストを作成します。これはテストにノイズが入るのを防ぎますが、このようなテストで実質的にわかるのは、アプリケーションがクラッシュしないことだけです。残念ながら、アプリケーションが想定された処理を実行しているかどうかを示すものではありません。この点は非常に重要です。

次世代のツールは、自動的に取得できる特定の値に基づいてアサーションを作成します。しかし、自動生成によって大量のアサーションを作成すると、大量のノイズが発生します。中間はありません。メンテナンスが容易だが無意味なテスト、あるいは価値が疑わしくメンテナンスは悪夢のようなテストのどちらかです。

ユニットテストの自動生成を行う多くのツールは、利用しだすとカバレッジが急に上昇するので、最初は役に立つように見えます。実際の問題が発生するのはメンテナンスです。開発者は、テストスイートを「きれいに」するために、自動生成されたアサーションを微調整することがよくあります。しかし、アサーションは脆弱であり、コードが変更されると対応できません。これは、次回のリリース時に開発者が再び「自動」生成を繰り返す必要があることを意味します。テストスイートは再利用するためのものです。再利用できない場合は、何かが間違っています。

そのほかにも、最初に高いカバレッジを達成しても、テストに含まれるアサーションがそもそもあまり意味のないものかもしれないという、より恐ろしい可能性があります。何かをアサートできるというだけでは、アサートするべきだとも、するのが適切だとも言えません。

public class ListTest {
        private List<String> list = new ArrayList<>();
        @Test
        public void testAdd() {
                list.add(“Foo”);
                assertNotNull(list);
        }
}

理想的には、アサーションはコードが正常に機能しているかどうかをチェックし、コードの動作が不適切であればアサーションが失敗します。このどちらの条件も満たさないアサーションが大量にあるという状況に陥るのは本当に簡単です。これについては次に述べます。

カバレッジvs.意味のあるテスト

確かな、意味のある、クリーンなテストスイートを犠牲にして、高いカバレッジの数値を追い求めるのは、本末転倒です。よくメンテナンスされたテスト スイートは、コードに対する信頼感をもたらし、迅速かつ安全にリファクタリングするための基礎にもなります。ノイズの多いテストや意味のないテストは、テストスイートを信頼できないこと、リファクタリングどころか、リリースの指標にさえならないことを意味します。

コードを測定したとき、特に厳しい基準に照らして測定したときにありがちなのは、自分たちの水準が思っていたよりも低いと判明することです。その結果として、カバレッジの数字を追いかけるようになります。カバレッジを上げよう!そうして、自動生成で有意義なテストが作成されたはずだと思い込んだり、意味がほとんどなく、メンテナンスのコストが高いユニットテストを手作業で作成するという、危険な領域に入り込んでしまうのです。

現実の世界では、テストスイートのメンテナンスにかかる継続的なコストは、ユニットテストの作成コストをはるかに上回るため、初めにクリーンで良質なユニットテストを作成するのが重要です。継続的インテグレーション(CI)プロセスの一環として毎回テストを実行できるかどうかで、ユニットテストが良質かどうかがわかります。リリース時だけにテストを実行しているのであれば、望ましい水準よりもテストのノイズが多いというしるしです。そして皮肉なことに、ノイズの多いテストはメンテナンスされないため、テストをさらに劣化させます。

自動化は悪いことではありません ―― 実際のところ、今日の開発の複雑さ、納期のプレッシャーを考えれば、自動化は必須です。しかし、値の自動生成は、メリットよりも面倒が多いのが通常です。むやみなアサーションの生成よりも、値の拡張、実システムのモニタリング、複雑なフレームワーク、モック、スタブの作成に基づく自動化の方が、より大きな価値を提供します。

対策

測定

最初のステップは、現在のカバレッジを測定することです。そうでなければ、現在の状況も、状況が改善しているかどうかもわかりません。その際には、ユニットテスト、機能テスト、手動テストなど、すべてのテストのカバレッジを測定し、適切に集計することが重要です。そうすると、最も効果の高い場所――つまり、エンドツーエンドのテストではカバーされているが、たまたまユニットテストではカバーされていないコードではなく、まったくテストされていないコード――に労力を集中させることができます。Parasoft DTPは、さまざまなタイプの複数回のテスト実行からコードカバレッジを正確に集約して、現状を正確に測定することができます。詳しくは、ホワイトペーパー「 Comprehensive Code Coverage: Aggregate Coverage Across Testing Practices 」(訳注:英語のページにジャンプします)を参照してください。

フレームワーク

ユニットテストのスケルトンを作成してくれるツールは、手始めとして良い手段です。実際のコードは複雑で、スタブやモックを必要とするため、ツールがMockitoやPowerMockのような標準的なモックフレームワークを利用できることを確認してください。しかし、それだけでは不十分です。以下を実行できる必要があります:

  • 意味のあるモックを作成する
  • 簡単なテストをより大量で広範囲のデータで拡張する
  • 実行中のアプリケーションをモニタリングする

インテリジェントな支援

これらのすべてを手動で行うことも不可能ではありませんが、時間と労力がかかりすぎます。ここは、自動化を活用するのが最適です。たとえば、Parasoft Jtestの新しい単体テストアシスタントは、既存のオープンソースフレームワーク(JUnit、Mockito、PowerMockなど)を活用してユニットテストスイートの作成、スケールアップ、メンテナンスをサポートすることでユニットテストプロセスを通じてユーザーをガイドし、カバレッジを増大させます。

何年も前、Parasoftは完全な自動生成という分野に手を染めたこともありますが、ROIが期待されたほどではないことがわかったので、ユニットテスト作成支援に立ち返り、テストが意味のある値を持つかどうかの確認にユーザーの頭脳を必要とする代わりに、残りの作業を自動化するソリューションに注力しています。Jtestの単体テストアシスタントの詳細については、最近のブログ「単体テストが嫌われる理由と愛を取り戻す方法」を参照してください。

まとめ

カバレッジが課題になっているのであれば、カバレッジを正しく測定し、また実行したすべてのテストからすべてのカバレッジを測定するようにしてください。カバレッジを増加させるためにユニットテストを追加する場合は、テスト作成支援ツールを活用すると、テストをすばやく作成/拡張し、メンテナンスが容易で意味のあるコードカバレッジを実現できます。Parasoft Jtestの単体テストアシスタントは、コードの拡張や変更に伴うメンテナンスが容易なテストを作成するので、同じ作業を何度も繰り返す必要がありません。

 

Parasoft Jtestについて

Java対応静的解析・単体テストツール Parasoft Jtest

Jtestは、テスト工数の大幅削減とセキュアで高品質なJavaシステムの開発を強力にサポートするJava対応テストツールです。1,000個以上のコーディング規約をもとにソースコードを静的に解析し、プログラムの問題点や処理フローに潜む検出困難なエラーを検出します。さらに、JUnitを用いた単体テストについて、作成、実行、テストカバレッジ分析、テスト資産の管理といった単体テストに係る作業をサポートし、単体テストの効率化を促進します。

Top