はじめに
この記事の内容は『単体テストの考え方/使い方』(Vladimir Khorikov 著)を自分なりに集約したものです。
重要な部分を見直しやすいよう細かい部分は飛ばして書いているので、詳細な説明や補足が気になる方は書籍を手に取ってご確認いただけますと幸いです。
目次
- 単体テストとは
-
古典学派とロンドン学派
2-1. 古典学派が考える単体テスト
2-2. ロンドン学派が考える単体テスト - 古典学派とロンドン学派の共通認識
-
古典学派とロンドン学派のテストコード
4-1. 古典学派のテストコード
4-2. ロンドン学派のテストコード -
依存への対処
5-1. 依存の種類
5-2. 依存関係のまとめ
単体テストとは
単体テストの定義については学派によって意見が分かれている部分がありますが、各学派の共通の認識として単体テストには次のような特徴があります。
- 少量のコードを検証する
- 実行時間が短い
- "隔離"された状態で実行される
古典学派とロンドン学派
プログラムのテストには大きく分けて古典学派とロンドン学派という2つの学派がありますが、両学派の意見が分かれる理由は"隔離"に関する解釈が違うためです。
古典学派が考える単体テスト
古典学派が考える"隔離"は「コードを意味のあるまとまり(Unit)に分けること」を意味します。
したがって、古典学派の解釈によれば単体テストは以下のようになります。
- 1単位の振る舞いを検証する
- 実行時間が短い
- 他のテスト対象の振る舞いから隔離された状態でテストを実行する
なお、今後特に断りがない限りこのシリーズでは単体テストの定義を古典学派の定義によるものとします。
補足
古典学派はテスト対象のコードをクラスや関数などの明確な単位で分けないため、隔離の結果に裁量の余地が生まれてしまいます。また、テスト対象を明確な単位に分けないため、テストが失敗した場合に原因箇所を特定するのが少し手間になります。しかし、それらのデメリットは以下の理由から特に問題になりません。-
設計の問題に気付ける
多くの場合、テスト対象のコードが隔離しにくい原因は設計にあります。そのため、テスト対象コードが上手く隔離できない場合は設計を見直せば解決する場合が多くあります
また、もしどうしてもテスト対象コードが隔離しにくい場合、その部分のテストは単体テストではなく統合テストで検証すべきとする判断もできます -
テスト対象コードの価値に目を向けられる
テスト対象をコードの意味から考ればテスト対象コードの価値に気付きやすくなります。テストの価値はテスト対象のコードの価値に基づくため、盲目的にすべてのクラスや関数をテスト対象にするよりも質の高いテストを実施できる可能性が高くなります -
テストの価値をお客様に説明しやすい
テスト対象をコードの意味から考えるため、非エンジニアにもテストの内容や価値を説明しやすくなります
以上の理由からこの記事では単体テストを古典学派の解釈に沿って定義することにしています。
ロンドン学派が考える単体テスト
一方、ロンドン学派が考える"隔離"は「コードをクラスや関数などの単位(Unit)に分けること」を意味します。
よって、ロンドン学派の解釈によれば単体テストは以下のようになります。
- 1つのクラスや関数を検証する
- 実行時間が短い
- 他のテストケースから隔離された状態で実行される
補足
ロンドン学派の隔離には次のようなメリットがあります。-
テスト・ダブルやオブジェクト・グラフをシンプルかつ軽量に作成できる
-
古典学派より細かな粒度で検証ができる
1つのテストケースで1つのクラスしか検証しないので検証の粒度が細かくなります -
依存関係が複雑になっても簡単にテストできる
すべての協力者オブジェクトをテスト・ダブルに置き換える方針なので個々の依存のことを深く考えなくていい -
テストが失敗した際、どの機能に問題があったのかを正確に見つけられる
すべての協力者オブジェクトをテスト・ダブルに置き換えており、テスト対象のクラスのみ扱っているので原因の特定がしやすい
しかし、ロンドン学派の解釈はプロダクト全体で評価するとあまり有用とは言えません。それは以下のような理由からです
-
テストの粒度を上げることにあまり価値がない
テストの目的は、1つ1つのクラス等の振る舞いを検証しなくても、1つの意味を持ったまとまりの振る舞いが検証できれば達成できます -
非開発者には理解しにくいテストになりやすい
テスト対象コードをクラスや関数などの単位で分ける場合、そのテストの意味は開発者には理解できても非開発者には理解しにくいことが多くなります。
単体テストですべきことはそのテストに関わる人達にテスト対象コードが解決しようとしている物語を伝えること、つまり凝集度を高めて非開発者でも理解できるようにすることです<凝集度の高い検証内容>
例:「私が犬の名前を呼ぶと、その犬は私のところに寄ってくれます」
<凝集度の低い検証内容>
例:「私が犬の名前を呼ぶと、その犬は左前脚を前に動かし、続いて左後脚を...」
用語📝テスト・ダブル
テスト対象オブジェクトが依存するオブジェクト(協力者オブジェクト)と同じ見た目・振る舞いをする簡潔なオブジェクトテスト・ダブルは、テスト環境でテスト対象オブジェクトをほかのオブジェクトから隔離した状態で動かすために使用されます。
用語📝オブジェクト・グラフ
同じ問題を解決するために結び付いたオブジェクトの集まり 例> テスト・ダブルがさらに別のテスト・ダブルに依存することで出来るオブジェクトのつながりなど古典学派とロンドン学派の共通認識
ここまでは古典学派とロンドン学派の違いに注目してきましたが、両学派には次のような共通部分もあります。
-
テストコードはAAAパターンを採用している
詳細
次の3フェーズで構成されるテストコード- 準備(Arrange)
- 実行(Act)
- 確認(Assert)
-
依存関係をテスト・ダブルで置き換える
詳細
テスト対象オブジェクトを他のプロダクトコードから隔離した状態で動かしたい場合、依存関係をテスト・ダブルというテスト用オブジェクトで置き換えます
-
不変なオブジェクトはテスト・ダブル化しない
詳細
定数やEnumデータ、"5"などの直書きデータは依存関係にあるとは認めません
古典学派とロンドン学派のテストコード
以下のテスト対象オブジェクトを題材に古典学派とロンドン学派のテストコードを比較してみましょう。
# 協力者オブジェクト
class Store:
def __init__(self) -> None:
self.inventory: dict = {}
def get_inventory(self, item) -> int:
return self.inventory[item] if item in self.inventory else 0
def has_enough_inventory(self, item: str, count: int) -> bool:
return self.get_inventory(item) >= count
def add_inventory(self, item: str, count: int) -> None:
self.inventory[item] = self.get_inventory(item) + count
def sub_inventory(self, item: str, count: int) -> None:
self.inventory[item] = self.get_inventory(item) - count
# テスト対象オブジェクト
class Customer -> bool:
def purchase(self, store: Store, item: str, count: int) -> bool:
if store.has_enough_inventory(item, count):
store.sub_inventory(item, count)
return True
else:
return False
古典学派のテストコード
題材のテスト対象オブジェクト(Customerクラス)はStoreクラスに依存しています。しかし、古典学派は1つの意味的にまとまったコードをテスト対象とするのでStoreクラスも含めて実行するテストコードを書きます。
# テストコード(在庫が十分ある場合、購入は成功するか)
def purchase_succeds_when_enough_inventory():
# 準備(Arrange)
store = Store()
store.add_inventory("shampoo", 10)
customer = Customer()
# 実行(Act)
success = customer.purchase(store, "shampoo", 5)
# 確認(Assert)
assert success is True
assert store.get_inventory("shampoo") == 5
# テストコード(在庫が十分でない場合、購入は失敗するか)
def purchase_fails_when_not_enough_inventory():
# 準備(Arrange)
store = Store()
store.add_inventory("shampoo", 10)
customer = Customer()
# 実行(Act)
success = customer.purchase(store, "shampoo", 15)
# 確認(Assert)
assert success is False
assert store.get_inventory("shampoo") == 10
def _test():
purchase_succeds_when_enough_inventory()
purchase_fails_when_not_enough_inventory()
if __name__ == "__main__":
_test()
ロンドン学派のテストコード
一方、ロンドン学派はテスト対象オブジェクトを厳密にクラスや関数に位置付けるため、協力者オブジェクトであるStoreクラスを"モック"というテスト・ダブルの1種などで置き換えます。
用語📝モック
モックとは、テスト対象オブジェクトと他のオブジェクトとの連携をシミュレートするための仮想的なオブジェクトです。例えば、あるオブジェクトが外部データベースとやり取りする場合、実際のデータベースへのアクセスを待っているとテストが遅延してしまいます。また、テストの結果はデータベースの状態によって変わる場合もあります。そこで、純粋にテスト対象オブジェクトの振る舞いだけを検証したい場合はモックデータベースを使用してテストを実行し、本物のデータベースと同じ振る舞いを模倣させます。
モックはテスト対象オブジェクトを他のオブジェクトから隔離して動かせるため、特にロンドン学派のテストでは非常によく使われます。
from unittest.mock import Mock # モック作成用ライブラリ
# テストコード(在庫が十分ある場合、購入は成功するか)
def purchase_succeds_when_enough_inventory():
# 準備(Arrange)
store = Mock(Store)
store.has_enough_inventory.return_value = True # 在庫は十分と仮定
customer = Customer()
# 実行(Act)
success = customer.purchase(store, "shampoo", 5)
# 確認(Assert)
assert success is True
assert store.has_enough_inventory.assert_called_once_with("shampoo", 5) is None
# テストコード(在庫が十分でない場合、購入は失敗するか)
def purchase_fails_when_not_enough_inventory():
# 準備(Arrange)
store = Mock(Store)
store.has_enough_inventory.return_value = False # 在庫は十分でないと仮定
customer = Customer()
# 実行(Act)
success = customer.purchase(store, "shampoo", 15)
# 確認(Assert)
assert success is False
assert store.has_enough_inventory.assert_called_once_with("shampoo", 15) is None
assert store.sub_inventory.assert_not_called() is None
def _test():
purchase_succeds_when_enough_inventory()
purchase_fails_when_not_enough_inventory()
if __name__ == "__main__":
_test()
依存への対処
テストコードのサンプルで見たように、ロンドン学派は依存関係を原則的にテスト・ダブルに置き換えます。しかし、依存の種類によっては古典学派でも協力者オブジェクトをテスト・ダブルで置き換える必要がありますし、逆にロンドン学派でもテスト・ダブルに置き換えない依存も存在します。
テストを計画する上ではテスト対象オブジェクトの依存関係をどのように捉えるかが重要になるため、ここからはオブジェクトの依存関係の種類をいくつか紹介していきます。
依存の種類
共有依存
共有依存は、DBのフィールドやサーバのファイルなど、特定のデータを複数のテスト対象オブジェクトが参照している状態の依存関係です。
例:DBのフィールド、サーバのファイルなど
補足
DBのフィールドやファイルが保持するデータは他のプロセスによって更新される可能性があります。そのため、古典学派もロンドン学派も基本的には共有依存はテスト・ダブルに置き換えるという対応を取ります。ただし、古典学派では単体テストにおいて隔離すべきなのはコードではなくテストケースであるという考えがあるため、複数のテスト対象オブジェクトを同時に実行しても問題にならないようにテストケースをまとめ、共有依存をテスト・ダブルで置き換えないケースもあるようです。
プライベート依存
プライベート依存は、特定のデータが1つのテスト対象オブジェクトにしか参照されていない状態の依存関係です。
補足
プライベート依存ではテスト対象オブジェクトの実行が他のテスト対象オブジェクトの振る舞いに影響を与えることはありません。そのため、古典学派では原則として依存関係をテスト・ダブルには置き換えません。一方、ロンドン学派はプライベート依存が可変依存(参照データがDBやファイルシステムで管理されていて変化し得る)の場合は依存関係をテスト・ダブルに置き換え、不変依存(参照データが定数やEnumデータなどで変化し得ない)の場合は置き換えないという対応を取ります。
可変依存
可変依存は、テスト対象オブジェクトに参照されているデータが可変である状態を意味します。
補足
可変依存が意味するところは参照データが可変であることだけなので、可変依存をテストダブルに置き換える必要があるか否かはそのデータが共有依存の状態かプライベート依存状態かに左右されます。不変依存
不変依存は、テスト対象オブジェクトに参照されているデータが不変である状態を意味します。
補足
不変依存のデータは仮に複数のテスト対象オブジェクトから依存されていても他からの影響を受けないので、テスト・ダブルに置き換える必要はありません。プロセス外依存
プロセス外依存は、プロダクトコードのプロセス外で発生する依存関係です。
補足
プロセス外依存の例にはDBのフィールドなどが挙げられますが、この分類は飽くまで依存関係がプロダクトコード上で発生するかによるので、例えばDBを異なるDockerコンテナ上で動かす場合は共有依存であってもプライベート依存とみなす場合があります。揮発性依存
揮発性依存は、プロダクトコードだけでは振る舞いが定まらないような依存関係です。
揮発性依存には次のようなパターンがあります。
- 設定ファイルなどがあり、設定に応じて振る舞いが変化するパターン
- ランダムな値を発生させるなど、呼び出すたびに振る舞いが変化するパターン
依存関係のまとめ
以下は古典学派とロンドン学派の依存への対処方針をまとめたものです。
- 古典学派は共有依存をテスト・ダブルに置き換える
- ロンドン学派は共有依存と可変依存をテスト・ダブルに置き換える
よって、依存関係を捉えるときは以下のポイントを意識するといいでしょう。
- その依存は共有依存かプライベート依存か
- (プライベート依存の場合、)その依存は可変依存か不変依存か
なお、以上のポイントは下図のような二分木でイメージできます。
単体テストと統合テスト
古典学派とロンドン学派はいずれも「統合テスト=単体テスト以外のテスト」考えていますが、古典学派とロンドン学派の間では単体テストの定義が異なるため、結合テストの定義も変わります。
まず、ロンドン学派では、実際の協力者オブジェクトを使うテストはすべて統合テストとみなします。そのため、古典学派が考えるほとんどの単体テストはロンドン学派にとっては統合テストに分類されます。
しかし、本シリーズでは基本的に古典学派の解釈を採用するので、以降、統合テストとは次の単体テストの特徴をいずれか1つでも欠いたテストを指すものとします。
- 1単位の振る舞いを検証する
- 実行時間が短い
- 他のテストケースから隔離された状態で実行される