目次
- 単体テストの目的
-
単体テストの評価指標
1-1. コード網羅率
1-2. 分岐網羅率
1-3. テストケースの価値
はじめに
この記事の内容は『単体テストの考え方/使い方』(Vladimir Khorikov 著)を自分なりに集約したものです。
重要な部分を見直しやすいよう細かい部分は飛ばして書いているので、詳細な説明や補足が気になる方は書籍を手に取ってご確認いただけますと幸いです。
単体テストの目的
単体テストの目的は開発を持続可能なものにすることです。
詳細
プログラムは1度作ったらそれで終わりではなく、通常は保守や機能追加を繰り返して成長し続けて行きます。しかし、もしここでテストを導入していないプロジェクトがあったとすると、そのプロジェクトでは改修の影響を受ける箇所のハンドリングが漏れてどんどんプログラムが無秩序化していきます。するとそのプロジェクトはデバッグなどの工数が増えて成長スピードが極端に遅くなり、最悪の場合は開発が続けられなくなってしまいます。単体テストは以上のようなプログラムの無秩序化を防ぎ、開発を持続可能なものにするため導入されます。
📝ソフトウェア・エントロピー
エントロピーは「無秩序の量」という意味で、プロジェクトの成長曲線で見るような開発スピードの鈍化はソフトウェア・エントロピーの増加が原因と言われている単体テストの評価指標
良い単体テストとは、端的に言えば良質なテスト・スイートを実行するテストです。
テスト・スイートとは、テストの目的や対象の機能ごとにテストケースを集めたもので、その品質を評価する指標には次のようなものがあります。
コード網羅率
コード網羅率は、テストを実行することでどれだけ多くのコードが実行されるかを評価する指標であり、次の式で表現できます。
${コード網羅率} = \frac{テストで実行されるプロダクションコードの行数}{プロダクションコードの行数}$
コード網羅率の捉え方
基本的にコード網羅率は高い方が望ましいですが、必ずしも100%である必要はなく、与えられた工期とのバランスを考えて、重要な部分の網羅率から上げていくことが求められます。
また、コード網羅率に囚われることは危険な場合もあります。
例えば、以下のプログラムのコード網羅率は3/4で75%になります。
# プロダクションコード
def isLongText(text):
if len(text) > 5: # プロダクションコード1行目(テスト1行目)
return True # プロダクションコード2行目
else: # プロダクションコード3行目(テスト2行目)
return False # プロダクションコード4行目(テスト3行目)
# テストコード
def test():
assert isLongText("abc") is False
しかし、ここでif分岐を三項演算に書き換えると、コード網羅率は1/1で100%になります。
# プロダクションコード
def isLongText(text):
return True if len(text) > 5 else False
# テストコード
def text():
assert isLongText("abc") is False
このように、コード網羅率は飽くまで実行された行数を評価だけのものです。そのため、コード網羅率を上げるだけではテストの品質を保証することはできません。
【コラム - コード網羅率100%を目指した結果】
ある会社はコード網羅率100%を目指しコード網羅率が100%に満たないものはレビューを通さないという厳しいルールを設けました。すると、プログラマー達は三項演算の例のような網羅率を上げるコードの書き方を編み出すようになり、期待した成果が全く得られず、その後レビュー基準を90%、80%と下げて行くも効果が現れず、ついにその会社からコード網羅率の規定はなくなったそうです。
分岐網羅率
分岐網羅率は、テストを実行することでどれだけ多くの分岐が実行されたかを評価する指標であり、次の式で表現できます。
${分岐網羅率} = \frac{テストで実行された分岐経路の数}{分岐経路の数}$
分岐網羅率の捉え方
分岐網羅率も基本的には高い方が望ましいですが、必ずしも100%である必要はなく、与えられた工期とのバランスを考えて重要な分岐経路の網羅率から上げていくことが求められます。
分岐網羅率はコード網羅率の欠点を克服するために導入された指標なので、コードの書き方には依存せず純粋にどれだけのパターンを網羅したかを評価できます。
# プロダクションコード
def isLongText(text):
return True if len(text) > 5 else False # 分岐網羅率 = 1/2 = 0.5
# テストコード
def test():
assert isLongText("abc") is False
しかし、分岐網羅率にも次のような欠点があるため、分岐網羅率だけでテストの品質を評価することはできません。
-
すべての実行結果を検証したことを保証できない
分岐網羅率が評価するのは純粋に分岐経路の網羅率だけです。そのため、次の例のように分岐先で実行された結果が本当に想定通りになっているかは別途テストコードを書かないと保証できません# プロダクションコード WasLongText = False # 直近の文字列が6文字以上かを保持する def isLongText(text): result = Ture if len(text) > 5 else False WasLongText = result return result # テストコード(分岐網羅率100%) def test(): assert isLongText("abc") is False assert isLongText("abcdef") is True # ※WasLongTextの更新結果が検証されていない # テストコード(分岐網羅率100%+検証コード) def test(): assert isLongText("abc") is False assert WasLongText is False assert isLongText("abcdef") is True assert WasLongText is True
-
ライブラリ内の分岐経路まで網羅することは難しい
詳細
例えば任意の型のデータを与えるとそれを文字列に変換して返す関数があるとします。そして、その関数に与えるデータ型が整数型などの基本的な型だけでなく独自定義のクラスなども含めるとすると、網羅すべきパターンは無数に増えて行き、それらの分岐経路をすべて網羅することは現実的にではなくなります。
テストケースの価値
コード網羅率や分岐網羅率が低いことはテスト・スイートの品質が低いことは示せます。しかし、網羅率が高いことはテスト・スイートの品質の高さを保証することはできません。
品質の良いテスト・スイートを作成するには次のような特徴を持ったテストケースを集めることに注力しましょう。
-
重要な部分がテスト対象になっていること
詳細
テストの価値はテスト自体が持っているわけではなく、テストによって検証されるコードにあります。つまり、重要なのは単体テストにかける労力をプログラムにとって重要な部分に向けることであり、重要な部分のテストケースを集めることがテスト・スイートの品質を高めることになるわけです -
開発サイクルの中で常に実行できること
詳細
テスト・スイートはコードの変更が想定外の結果をもたらしていないかを確認するためにあります。したがって、テスト・スイートに含まれるテストケースは開発サイクルの中で常に実行できるものであることが重要です