コードカバレッジ100%はベストなのか

に公開

はじめに

横浜銀行アジャイル開発チームのwatariです。
皆さん、ユニットテストはされていますでしょうか。

ユニットテストも色々ですが、今やJUnitやVitestなど用いてテスト自動化を求められる現場がほとんどでしょう。
2010年代近辺から発表されたモダンな言語はそれ自体にテスト機構がついているものばかりです。

そして自動テストの品質担保の指標の1つとして良く語られるのが、コードカバレッジです。
カバレッジツールは、Java系だとJaCoCoなど有名ですね。
このカバレッジを100%、つまりすべてのコードを走査・テストしていることで、「品質は大丈夫です!」 とすることへの疑義が本記事のテーマになります。

なお、C0、C1などカバレッジ指標はここでは扱いません。

そして書きつくされた話題ではありますが、特に以下を参考としています。
ソフトウェアテスト徹底指南書 〜開発の高品質と高スピードを両立させる実践アプローチ

前提

カバレッジの話の前にテスト設計について。
なぜカバレッジを取るのか、という前提になります。

まず、ユニットテストもテスト設計は必要なので、そこから見ていきます。
テスト方針です。

どのようにテストコードを書いていくか

  • 自動テスト
    • テスト自動化において、テストコードも保守されるべきソフトウェアである
    • テストコードそれ自体で、テスト設計書・手順書相当の内容は表現する
    • テストは仕様書のように書く

最後の「テストは仕様書のように書く」の補足をすると以下のように、テストケースが仕様を表現しているようなものです。

class Test_is_leap_year(unittest.TestCase):
    def test_400で割り切れる年はうるう年である(self):
        self.assertEqual(True, is_leap_year(2000))

    def test_400で割り切れず100で割り切れる年はうるう年でない(self):
        self.assertEqual(False, is_leap_year(200))

逆に、コードの内部構造からテストを作っていくと、テストコードのあるべき姿を保証しづらいです。
ユニットテストは、品質のため以下のようにあるべきと考えています。

  • 冗長さがないテストコード。的確にテストの目的を満たしている
  • テストの意図が明確で、変更時にどのテストをどう保守すればよいかわかる
  • テストが安定している。壊れにくいテスト

内部構造からテストを作る、制御フローテストのようなもののすべてを否定しませんが、以下の問題を引き起こしやすいです。

  • コードのふるまい、仕様とは無関係のテスト設計を誘発する
  • テストコードを読んでもなんのためのテストか分かりにくい状態を引き起こす
  • ソフトウェアの価値に貢献しない無駄なテストを許してしまう

詳細なテスト設計のアンチパターン

筆者の経験則から来るものです。
設計において、すべきでないことで、カバレッジ100%にも通じる内容です。

  • テスト設計書・仕様書
    • 対象の数、ケースともに多く、すべてに詳細を詰めると大きなコストとなる
    • 実装・ユニットテストは、開発ライフサイクルの中でも変更が多い。詳細なテスト仕様書はその変更に対し大きな保守コストを払う

踏まえて、基本的には以下のようにすると良いです。

  • ユニットの振る舞いやインターフェースの仕様が実現されている
  • なんとなく危ない、と思った箇所が保護されている、許容できる程度の水準にある
  • 何らか、案件によって特別なテスト要求がある

これら方針のもとテスト技法(境界値分析やデシジョンテーブルなど)を用いて設計を進めます。
テスト技法を書いていくと、記事がとても長くなるのでここでは割愛します。

このテスト設計の方針をベースに、カバレッジの活用を見ていきます。

カバレッジ活用(カバレッジが十分にあるとき)

まずユニットテストで確認したいことは以下のはずです。
仕様が網羅されている

それこそコードの分岐が網羅されているではないはずです。

コードカバレッジは、以下のように分析に使います。
それ自体が品質の担保ではありません。

  • 仕様に対し網羅的なテストを作ったのにコードカバレッジが低い
    • 仕様から外れた冗長なコードがないか
    • 実行できないデッドコードがないか
    • 明示されていない暗黙の仕様がある
    • コードのテスト容易性に問題があり、カバレッジが低くなる
      • これはソフトウェア設計や実装のテクニックにかかるので、深掘りしません
        ただ、クラスやメソッドの独立性が低いと、起こりうる問題です
  • 継続的に実行しているテストのコードカバレッジが下がった
    • テストを書かずにプロダクトコードを追加・変更している
    • テストが意図通り実行されていない(テストのバグ)

カバレッジ活用(カバレッジが十分にないとき)

筆者が、かつて自動テストが1件もない現場にいたときの話です。
※その現場ではPostmanを用いてユニットテストとしていました。
 jsonの定義ファイルはメンテナンスされておらず、まず認証が通りませんでした。

品質面でもリソース面でもハッピーではないので、自動テストを追加していくのですが、以下の方針としていました。

  • アクセスの多いAPIの、ビジネスロジックの正常系をまず作る
  • ビジネスにおいて重要なバッチ処理の、正常系を作る
  • 開発も並行するが、その案件において機能変更があったAPIに関連するテストは正常系、異常系ともに作成する

私は方針のみ決めましたが、設計はその有無さえも開発者各々に任せた結果、テスト品質がばらついてしまいました。
振り返りとして大いに改善の余地があるものですが、方針は誤ったものではなかったと考えています。
カバレッジも、コードが膨大だったのもあり全体で見ると最初は数パーセントでしたが一応は改善を見せました。

具体例としてはこのようなものですが、まとめていくと以下のようになります。

  • テスト分析・設計をやり直す
  • 仕様や品質リスクを推定し、それに対してどのようなテストケースが必要か設計する
  • 上記に基づいてテストコードの整理や追加を行う
  • 上記の結果としてコードカバレッジが改善していることを確認する

カバレッジ目標の設定

記事のタイトルからお察しですが、100%は難しいです。
ユニットテストで実行しにくいコード、行う必要性の低いコードなど存在するためです。
ただ、理想ではあります。

何%以上が良い、とは調べると出てくるものですが、文章で表現するなら以下が良いでしょう。

  • プロダクトリスクに応じてコードカバレッジを設定する
    • リスクの高いコンポーネントでは高いカバレッジ
    • リスクの低いコンポーネントでは低いカバレッジ
  • カバレッジの種類(MC/DCカバレッジなど)は自動計測できるものを採用する
  • ほかのテストレベルとの組み合わせを考慮する
    • 統合テストでカバレッジを確保できるなら、ユニットテストのカバレッジをその範囲で緩和する
    • 逆に他のテストレベルでカバレッジを確保しにくい場合は、ユニットテストのカバレッジを高く設定する
    • テストピラミッドが逆転することはないように

まとめ

コードカバレッジは、テストの品質や網羅性を測る上で有用な指標ですが、「100%」という数字だけを追い求めることが目的化してしまうと、本来の品質保証や仕様の担保から逸れてしまう危険性があります。
内部構造を追いかけてテストを作ることも、また同じです。

仕様やリスクに基づいたテスト設計を行い、テストコードが本当にプロダクトの価値や品質向上に寄与しているかを常に意識しましょう。
カバレッジはその結果を確認するための“道具”として活用し、必要に応じて目標値を柔軟に設定すると良いです。

テストや品質保証の考え方は、現場やプロダクトごとに最適解が異なります。
本記事が、皆さんの現場でより良いテスト設計・運用の一助となれば幸いです。

横浜銀行(内製担当有志)Tech Blog

Discussion