はじめに
この記事の内容は『単体テストの考え方/使い方』(Vladimir Khorikov 著)を自分なりに集約したものです。
重要な部分を見直しやすいよう細かい部分は飛ばして書いているので、詳細な説明や補足が気になる方は書籍を手に取ってご確認いただけますと幸いです。
目次
-
価値あるテストを認識する
1-1. 価値あるテストの4本柱
1-1-1. 退行からの保護
1-1-2. リファクタリングの耐性
1-1-3. 迅速なフィードバック
1-1-4. 保守のしやすさ -
価値あるテストを作る
2-1. テストの正確性を上げるには
2-2. 理想的なテストにするには
価値あるテストを認識する
前回までの記事のおさらいになりますが、価値のあるテストには次の特徴があります。
-
テストすることが開発サイクルの中に組み込まれている
詳細
単体テストに価値が付くのはその単体テストが実際に使われている場合だけです。使われない単体テストを作成しても意味はありません
-
コードベースの特に重要な部分がテスト対象になっている
詳細
プロダクトコードのすべての部分について単体テストを実践する必要はありません。いいテストとはプロダクトの核となる部分(ドメイン・モデル)の振る舞いを検証するテストケースを集めたもの(テスト・スイート)です
以上の特徴を実現するにはまず価値のあるテストケースを認識する力が必要になります。
価値あるテストの4本柱
価値あるテストは以下の4つの要素をすべて持っている必要があります。
- 退行(regression)からの保護
- リファクタリングの耐性
- 迅速なフィードバック
- 保守のしやすさ
退行からの保護
開発における退行とは、プロダクトコードに何らかの変更を加えた後に既存の機能が意図した通り動かなくなることです。
補足
通常プロダクトコードは大きくなるほど潜在的なバグを抱えることになり、対処しなければならないことを増やします。そのため、残念ながらテストにおいてプロダクトコードは資産ではなく負債と言えます。したがって、テストにはプロダクトコードを退行から保護する役割が求められます。退行から保護すべきコード
プロダクトコードを退行から保護する必要性は次のようなポイントから判断できます。
-
実行されるコードの量
詳細
一般的にコードの量が増えるほど退行が見つかる可能性が高くなるため、実行されるコード量が多い場合には退行への注意が必要になります。
-
コードの煩雑さ
詳細
変数を定義するなどの基本的な処理や、ボイラーテンプレート・コードと呼ばれる何度も使い回すような処理にはバグが入り込むリスクが少ないですが、そうではない複雑なコードはバグの温床になる危険性があるため退行への注意が必要です。用語📝ボイラーテンプレート・コード
ボイラーテンプレートコードとは、何度も使われる汎用的なコードや基本的な構造を持つコードのことです。ボイラーテンプレート・コードはプロジェクトの初期段階で設定や準備をするためのコードを指すことが多く、その後の開発において同じパターンを再利用することができるとされています。例:ログイン機能、ユーザ登録機能など
-
ドメインの重要性
詳細
ビジネス・ロジック的に重要な機能ほど退行による被害が大きくなるため注意が必要になります。
Tips: 退行に対する保護を最大限に備えるには、外部ライブラリなども含め、できるだけ多くのプロダクションコードを実行させるようにしましょう
リファクタリングの耐性
リファクタリングの耐性とは、リファクタリングをしても「偽陽性(false positive)」と呼ばれる「嘘の警告」を上げない性質のことです。
補足
リファクタリングとは、既存のコードの振る舞いを変えることなく品質向上の目的でそのコードを変更することを言います。ですから、リファクタリングによってテストが失敗するということは、実際には正しい振る舞いをしているのにテストが間違いを指摘しているわけなので、「嘘の警告」を上げていることになります。リファクタリングの耐性がないテストコードは振る舞いではなくコードを検証してしまっているので、いいテストコードは必ずリファクタリングの耐性を持っている必要があります。
用語📝偽陽性
テストにおける偽陽性とは、実際にはテスト対象オブジェクトが意図した通りに振る舞っているのにテストコードの誤りによってテストが失敗してしまうことを言います。ちなみに「偽陽性」という言葉は統計用語から来ており、統計では「第一種の過誤」とも呼ばれています。
【コラム - 偽陽性がもたらした問題】
ある現場では既に十分なテスト・スイートが用意されていましたが、既存のコードをリファクタリングするたびにテストが失敗していました。最初の頃、開発者たちはテストの失敗にきちんと対処して原因を調べていたのですが、あるテストの失敗がテストコードの問題によるものだと判明した結果、開発者たちはそのテストの失敗を無視し、失敗したテストケース(テストコード)を無効化しました。というのも、このとき開発者たちは「失敗の原因がテストコードにあるなら、ひとまずそのテストコードは無効化し、プロダクションコードの開発を優先しよう」と判断してしまったのです。
こうして、しばらくプロダクトの開発は問題なく進んでいたのですが、あるとき深刻な影響を及ぼすバグが本番環境に持ち込まれていたことが発覚します。そして原因を調査したところ、実はテスト・スイートの中にはそのバグを検出するテストケースは備わっていたのですが、そのテストケースはテストコードの自体の問題により無効化されていたのです。
偽陽性を除く方法
偽陽性は、テストコードがテスト対象オブジェクトの振る舞いではなくコードや実装を検証している場合に発生します。したがって、テストコードがテスト対象コードの実装やアルゴリズムなどに結び付くとそのコードはリファクタリングの耐性を失うことになります。
偽陽性を回避するにはテストコードをテスト対象オブジェクトの内部的なコードから切り離し、プロダクトコードが最終的にもたらす結果だけを検証するようにしましょう。
# テスト対象オブジェクト
class Greeter:
def say_hello(self) -> str:
return "Hello!"
# テストコードがテスト対象コードを検証している例
def test_greeter_can_say_hello_by_implementation():
sut = Greeter()
msg = sut.say_hello()
assert isinstance(sut, Greeter)
assert hasattr(sut, 'say_hello')
assert msg == "Hello!"
# テストコードが振る舞いを検証している例
def test_greeter_can_say_hello_by_behavior():
sut = Greeter()
msg = sut.say_hello()
assert msg == "Hello!"
迅速なフィードバック
テストは開発サイクルの中で常に実施されるべきなので、テストが高速に完了することは非常に重要です。
補足
テストが高速に完了することには次のようなメリットがあります。- 実行するテストケースを増やせる
テストが十分高速に完了するなら、テストケースを増やしても問題なく、より質の高いテストを実践できる - テストの頻度を増やせる
テストが遅いとテストにかかるコストが上がりテストの頻度や回数が落ちてしまう恐れがあります。これはテストが適切に実施されていれば問題になりませんが、テストが面倒になり実施すべき場面で実施されないと後の工程にバグを持ち込む原因になります - バグの修正が早まる
テストのフィードバックが迅速に得られることは単純にデバックにかかる時間を短縮する効果があります。また、これはテストの頻度を増やせる利点とつながりますが、テストのフィードバックが迅速に得られることはバグを後の工程に持ち込むことを防ぎ、本来なら必要ない作業に手を取られることを回避できる場合があります
保守のしやすさ
テストコードの保守のしやすさは次のような観点から評価できます。
- テストケースの理解にかかる負荷
詳細
テストケースの理解にかかる負荷は、一般的にテストコードの量が多いほど上がります。 - テストを実施する難しさ
詳細
テストを実施する難しさは、テスト対象オブジェクトが依存している協力者オブジェクトの数や性質によります。これは依存先のオブジェクトがリモートDBや外部APIなどである場合を考えれば容易に想像できるでしょう
価値あるテストをつくる
テストの正確性を上げるには
価値あるテストの4本柱のうち「退行からの保護」と「リファクタリングの耐性」はテストの正確性に関係しており、信号対ノイズ比を用いるとその関係性は次のように表すことができます。
${テストの正確性} = \frac{信号(検出されたバグの数 = 退行からの保護の度合い)}{ノイズ(嘘の警告の数 = リファクタリングの耐性の低さ)}$
補足
リファクタリングの耐性で使った「偽陽性」という言葉は統計の用語です。統計では検証の結果を下表のように分類しており、偽陽性(第二種の過誤)はリファクタリングの耐性、偽陰性(第二種の過誤)は退行からの保護にそれぞれ関係しています。
振る舞い:正しい | 振る舞い:誤り | |
---|---|---|
テスト:成功 | 真陰性 | 偽陰性 (第二種の過誤) |
テスト:失敗 | 偽陽性 (第一種の過誤) |
真陽性 |
- 真陽性:実際に正しいことを正しいと判定すること
- 偽陽性:実際は正しいのに誤りと判定すること(リファクタリングの耐性に関係)
- 真陰性:実際に誤りなことを誤りと判定すること
- 偽陰性:実際は誤りなのに正しいと判定すること(退行からの保護に関係)
統計ではこの偽陽性と偽陰性が含まれる割合いからそのテストの正確性を評価します。
テストが嘘の警告を上げなかったとしても、バグを見つけられなければそのテストに価値はありません。また、反対にすべてのバグを検出できたとしても、嘘の警告ばかり出していたらそのテストは正確とは言えません。
テストの正確性を向上させるには、より多くのバグを検出できるようにするのとともに、可能な限り嘘の警告を出さないテスト・スイートの作成を心がけましょう。
理想的なテストにするには
再度おさらいになりますが、価値のあるテストには次の4つの要素がすべて備わっている必要があります。
- 退行(regression)からの保護
- リファクタリングの耐性
- 迅速なフィードバック
- 保守のしやすさ
理想的なテストを作るには上の4要素をそれぞれ最高の状態で備えるようにしたい所ですが、残念ながら現実的にはそのようなテストを作成することはできません。なぜなら、「退行からの保護」や「リファクタリングの耐性」の2要素と「迅速なフィードバック」は排反する内容だからです。
したがって、現実的には4要素の間に次のような優先順位をつけることが理想的な対応になります。
-
リファクタリングの耐性
詳細
リファクタリングの耐性はテストコードをテスト対象コードと「切り離す/切り離さない」のどちらかになるため、対応としては原則的に「切り離す」を選択しましょう
-
退行からの保護、迅速なフィードバック
詳細
退行からの保護と迅速なフィードバックはテストの段階によって優先順位が変わります。 テストの段階を「単体テスト」→「統合テスト」→「E2Eテスト」のように分けると、イメージとしては最初の単体テストでは迅速なフィードバックの評価が重視され、後のE2Eテストに進んでいくほど退行からの保護が優先されると考えます
-
保守のしやすさ
詳細
テストコードの保守のしやすさは他の3要素と競合しません。そのため、保守のしやすさは先の3要素に対処したあとで時間が許す限り取り組むめばいいでしょう
📝ホワイトボックステストとリファクタリングの耐性
ブラックボックステストとは、システムの機能を内部構造を知ることなしに検証するテスト手法です。そして、反対にホワイトボックステストはシステムが内部的に行っていることを検証するテスト手法を指します。この記事では基本的にテスト対象オブジェクトの振る舞いを検証するため、テストケースの作成には原則ブラックボックステストを採用します。しかし、ブラックボックステストだと見逃してしまうエラーも存在するため、内部構造の検証が必要な場合にはホワイトボックステストを採用することもあります。
だたこの時、ホワイトボックステストはテストコードをテスト対象オブジェクトの内部構造と結び付けるため、リファクタリングの耐性を損なう点には留意が必要となります。