💯

テストカバレッジ100%を実践した結果、100%を目指さない方がいいと理解した

2021/06/13に公開

プライベートで作っているサイトを題材に、テストカバレッジ100%を達成しました。やってみた結果、100%のカバレッジを目指さない方がいいと理解したので、それについて書きます。

作ったもの

完全に自分のためだけの、プライベートで作っているサイトです(なのでURLは載せません)。具体的には次のような機能があります。

  • 支出管理機能
  • 医療費管理機能
  • 買い物リスト機能
  • タスク管理機能
  • リンク集
  • など・・・

WebフレームワークにはDjango、データベースはPostgreSQL、デプロイ先はHerokuというよくあるWebアプリケーションです。Ajaxもほとんど使っておらず、単純なCRUD + List機能がほとんどです。

開発規模

開発規模はこれくらいです。

  • コード: 14,075行
    • プロダクションコード: 7,000行ほど
    • テストコード: 7,000行ほど
  • テンプレート: 1,307行
  • ビュー(MVCで言う「コントローラ」): 294
  • カスタムコマンド[1]: 8
  • モデルクラス: 47
  • テンプレートファイル: 62

Djangoにはクラスベースビューという機能があり、CRUD + Listといった基本的な機能については簡単に書きやすくなっています。これらのクラスベースビューをさらに拡張して、自分が使いやすいようにしています。あくまでサンプルコードですが、GitHubで公開しています(少し古いですが)。

また、ビューに比べてテンプレートファイルが少ないのは、テンプレートファイルを共通化することにより自前で書く必要性を減らしているからです。独自にテンプレートファイルを書いているビューは5.7%(17/294)だけです。

このプライベートサイトのコードに対して、カバレッジ100%を目指しました。

カバレッジ100%の基準

カバレッジ100%は次を前提としています。
対象外コードについては # pragma: no cover を付けて明示的に除外しています。

  • 基準: C1カバレッジ(分岐網羅)
  • カバレッジ対象
    • プロダクションコード
    • テストコード
    • テンプレートファイル(django_coverage_plugin 2.0.0を使用[2])
    • ValidationErrorなど、通常の操作で起きる可能性がある例外
  • カバレッジ対象外
    • Djangoが生成するマイグレーションファイル
    • 通常起きない例外
      • NotImplementedError
      • TemplateSyntaxError
      • ImproperlyConfigured
      • AssertionError
      • これらは「間違ってコードを書かないと起きない例外」なので対象外が適切です。
    • 外部API呼び出し
      • モックを使って呼び出しパラメータの検証ができるため、ここは少しサボりました。

テストコードの効率化

テストカバレッジ100%を目指すにあたり、テストコードを効率よく書けるようにしました。具体的にはCRUD + Listといった基本的な機能については対応する基底クラスを作成し、それを継承する形にしました。

例えばタスク管理機能の「プロジェクトを追加する機能テスト」はこのように書きます(GenericTestAddが基底クラス)。これにより、正常ケースだけでなく、必須項目がないとき、あるいは文字列が長すぎる場合のテストが宣言的に書けるようになっています。

class TestProjectAddView(GenericTestAdd):
    model = Project
    url = reverse("task:project:add")
    success_url = reverse("task:project:list")

    minimum_inputs = {
        "name": "プロジェクト1",
    }
    maximum_inputs = {
        "name": "プロジェクト1",
        "status": ProjectStatus.WIP,
        "category": CategoryFactory,
        "mode": ModeFactory,
        "url": "https://www.google.com/",
        "note": "備考",
    }
    invalid_values = {
        "name": ["a" * 101],
    }

基本的な機能についてはモデルクラス、ビュークラス、テストコードを自動で作成する、いわゆる scaffold 機能も作成しました。

〜95%: 普通に頑張ればいける

上記のようにテストコードを効率的に書けるようにしたため、カバレッジを計測した当初から92%ありました。

新機能開発を優先しテストを省略した結果、4月28日に最小で83%になりましたが、ちゃんとテストを書くことで、5月17日に95%まで上がりました。

「これなら100%いけるんじゃね?」と思い、100%を狙い始めましたが、そこからが大変でした。

〜99%: 「なぜ通らないか」を考える必要がある

95%を超えるとだんだんカバレッジをあげるのが苦しくなってきました。ただこのときは見返りがありました。なぜなら、例えば次のように、バグを修正したり、リファクタリングを行った結果としてカバレッジが上がったからです。

  • エラーメッセージが出ないバグに気づいて、正しく修正することでカバレッジを上げられた。
  • テンプレートファイルを共通化することで、微妙な差異がなくなり、カバレッジが上げられた。
  • 現在使われていないコードに気づいて削除することで、カバレッジを上げられた。

99%に達したのが6月1日、4%上げるのに15日かかりました。

〜100%: だんだん不毛な作業が増えてきた

99%を超えるとだんだん苦しくなってきました。このときにやった作業は次の通りです。カバレッジ100%を実現するために手段を選ばなくなっています。機能開発やバグ修正を後回しにし、品質も上がっていません。

  • 2行のカバレッジを増やすために26行のテストを書く
  • カバレッジを増やすために機能を増やす(本末転倒)
  • 2個の条件分岐を1行にまとめる(つまりC2カバレッジは上がらない)
  • 「フォールバック」として書いていたコードが通らないため、assertや、NotImplementedErrorに変える(フォールバックの意味がなくなる)

100%に達したのは6月11日、1%上げるのに10日かかりました。

まとめると次のようになります。だんだんペースが下がっているのが分かります。

  • 4/28: 83%
  • 5/17(20日後): 95%
  • 6/1(15日後): 99%
  • 6/11(10日後): 100%

カバレッジ100%を達成してわかったこと

Martin Fowler氏が書いた文書に次のように書かれています。

プログラミングの多くの側面と同じく、テストには思慮深さが必要だ。よいテストを選るのに、TDD は有用だが、決して十分ではない。思慮深くテストを実施すれば、テストカバレッジはおそらく80%台後半か90%台になるだろう。100%は信用ならない。カバレッジの数字ばっかり気にして、自分が何をやっているかわかっていない人間のいる臭いがする。

確かにその通りでした。

99%までは上げるのは難しいですが、見返りがありました。しかし、100%に上げるためにやった作業は実に愚かなもので、 カバレッジの数字ばっかり気にして、自分が何をやっているかわかっていない人間のいる臭いがする。 と言われても仕方がないことです。仕事でこれやったら間違いなく怒られるでしょう。

その一方で、ちゃんとテストを書くと、どれくらいのテストコードが必要かがわかりました。「テスト駆動開発」では次のように書かれています。

TDDをやっていると、テストコードとプロダクトコードの行数は、同じくらいになる。

TDDはやりませんでしたが、実際に書いたプロダクションコードと、テストコードはほぼ同じくらいの行数でした。言い換えると、カバレッジが低いにもかかわらずテストコードが多すぎるなら、無駄なテストを書いているか、テストコードの書き方が冗長な可能性が高いです。

これが体験として理解できたので、カバレッジ100%にしてよかったと思います。仕事でやるとダメですが、個人開発なので。

脚注
  1. コマンドラインから使えるもの。Djangoではこれを使ってバッチ処理などを実現できる。 ↩︎

  2. 2.0.0より前には、importなどが計測対象となっていなかった問題がありました。 ↩︎

Discussion