単体テストの定義
- 「単体(Unit)」と呼ばれる少量のコードを検証する
- 実行時間が短い(=全てのテストケースを想定している時間内に検証を終えられる)
- 隔離された状態で実行される
※この定義の意味は古典学派とロンドン学派で異なる
古典学派とロンドン学派の違い
隔離対象 | 単体(Unit)の意味 | テスト・ダブルに置き換える対象 | 開発方法 | |
---|---|---|---|---|
古典学派 | テストケース | 1つのクラス or 同じ目的を達成するためのクラスの1グループ |
共有依存 | 内側から外側に向かうテスト駆動開発 |
ロンドン学派 | 単体(1つのクラス) | 1つのクラス | 不変依存を除いた全ての依存 | 外側から内側に向かうテスト駆動開発 |
テスト・ダブルとは?
リリース対象のオブジェクトと同じような見た目と振る舞いを持ち、簡潔にしたオブジェクト
※厳密には異なるが、簡単に言うと「モック」のこと
隔離対象の違い
古典学派の考え方
各テストケースが隔離された状態で実行される
→各テストケースをお互いに影響を与えることなく、個別に実行できるようにしなければならない
- 各テストケースを隔離するメリット
- 複数のテストケースを同時や順番関係なく実行できるので、最も効率的な実行方法を選択できる
ロンドン学派の考え方
テスト対象から**協力者オブジェクト(依存するクラスなど)**が隔離された状態で実行される
→依存する全てのオブジェクトをテスト・ダブルに置き換えてテストする
- 協力者オブジェクトを隔離するメリット
- テスト対象の振る舞いは外部から隔離されるので、テスト対象のことだけに専念できる
- オブジェクト・グラフ(同じ問題を解決するために結びついたオブジェクトの集まり)を分離できる
古典学派では、依存が連鎖している場合もテストのために全ての依存オブジェクトを用意しなければいけない
→テスト・ダブルを使うと、単体テストを行うのに必要な準備の量を減らすことができる - テスト・スイート全体がシンプルな構造になる
「1つのテストケースは1つのクラスしかテストしない」という考え方なので、テストの際にコードベースをどのくらい網羅しなくてはいけないのかを考える必要がない
テスト・ダブルに置き換える対象の違い
テスト・ダブルに置き換える対象は、協力者オブジェクトがどのような依存状態なのかによって決まる
依存状態の種類
共有依存
テストケース間で共有される依存
例)静的(static)な可変フィールド・データベース
プライベート依存
テストケース間で共有されない依存
- 可変依存
状態(値)が変わるプライベート依存 - 不変依存
状態(値)が変わらないプライベート依存
プロセス外依存
アプリケーションを実行するプロセスの外で稼働する依存
プロセス外依存は共有依存である場合が多いが、常にそうであるとは限らない
- 読み込み専用のデータベース
データベースが書き換えられることはないので、共有依存ではない - 各テストケースごとにDockerコンテナで隔離されたデータベース
テストケースごとに異なるデータベースインスタンスを使っているため、共有依存ではない
揮発性依存
- 開発者のマシンにデフォルトでインストールされる設定に加えて、テスト対象のアプリケーションを適切に稼働させるのに独自の設定(利用するために追加の設定)を行うことが要求される依存
例)データベース・APIサービス - 呼び出すたびに異なる振る舞いを行う依存
例)ランダム値を生成するクラス・現在日時を返すクラス
揮発性依存と共有依存は異なる
例)ファイルシステムは、共有依存であるが揮発性依存でない
「テストケース間で共有される」→共有依存の性質
「開発者のマシンにデフォルトで用意されていて、ほとんどの場合は決まった振る舞いしかしない」→揮発性依存の性質ではない
古典学派の考え方
共有依存のみをテストダブルに置き換える
シングルトンパターン(指定したクラスのインスタンスが1つしか存在しないことを保証する)を採用している場合、シングルトンはプライベート依存になる
- なぜ共有依存をテストダブルに置き換えるのか?
- 複数のテストケースを同時に実行した時に、お互いの検証に影響を与えてしまい、正しい結果を得られなくなるため
- テストの実行速度を上げるため
共有依存かつプロセス外依存のオブジェクト(データベースやファイルシステムなど)は呼び出しに時間がかかる
→テスト・ダブルに置き換えることによって、テストの実行速度が上がる
古典学派では、共有依存ではないプロセス外依存のオブジェクト(読み込み専用のDBなど)が十分な実行速度を持つ場合、そのまま単体テストで使用しても良い
ロンドン学派の考え方
不変依存以外の全ての依存(共有依存と可変依存) をテスト・ダブルに置き換える
- なぜ不変依存以外をテストダブルに置き換えるのか?
- より細かな粒度(クラスごと)で検証ができる
オブジェクト指向プログラミングにおいて「クラス=全てのコードベースの基盤となる分解できない構成要素」と考える
→単体テストでも「テストで検証される分解できない構成要素=1つのクラス」と考える - 依存関係が複雑になっていても簡単にテストすることができる
- テストが失敗した際、どの機能に問題があったかを正確に見つけられるようになる
全ての協力者オブジェクトがテストダブルに置き換えられている
→テストが失敗した原因を調べる際にテスト対象システムだけを見れば良い
- より細かな粒度(クラスごと)で検証ができる
これらのメリットは本当にメリットと言えるのだろうか?
1. 「より細かな粒度(クラスごと)で検証ができる」について
単体テストで本当に見るべき点は?
× 1つのクラス
○ ビジネスサイドから見て有用であると考えられる振る舞い
→単体テストの粒度は、「ビジネスサイドから見て有用であると考えられる振る舞い」とするべき
具体的な考え方としては…
テストケースでは、そのテストに関わる人たちにテスト対象のコードが解決しようとしている物語を伝える
そのために、テストケースの凝集度を高め、非開発者でも理解できるようにする
古典学派とロンドン学派では「単体テストはプロダクションコードのことをどのくらい把握しなくてはならないのか」が異なる
→ロンドン学派の方が実装の詳細をテストする傾向にある
※1単位の振る舞いではなく、1つのクラス(分解できない構成要素)をテストするから
2. 「依存関係が複雑になっていても簡単にテストすることができる」について
複雑な依存関係を持つものに対する単体テストに対して、本来考えなければいけないことは何か?
× 複雑な依存関係を持つクラスを検証する方法を見つけること
○ 複雑な依存関係を構築しなくても済むようにする方法を見つけること
※複雑な依存関係が必要になっている場合、間違った設計が原因であることが多い
3. 「テストが失敗した際、どの機能に問題があったかを正確に見つけられるようになる」について
古典学派のスタイル(共有依存しかモックしない)では、どの機能に問題があったかを見つけられないのか?
→単体テストを頻繁に(コードが変更されるたびに)実施していれば、最後に修正した箇所がテストを失敗させたの原因であることが分かる
失敗がテストスイートのいたるところに広がることは必ずしも悪いことではない
「1つの問題が多くのテストケースに問題を与える=その問題のあったコードは多くのクラスに依存されている」ということ
→そのコードは重要な価値があることの証明になる
(設計の変更などでコードを扱う際に有益な情報となる)
単体テストコード例
テストケース
- 顧客が商品を購入する際に、在庫があれば取引成立で、在庫が購入数分減る
- 在庫がなければ取引失敗で、在庫は変わらない
協力者オブジェクト
-
Store
クラス
古典学派の場合
Store
クラスはインスタンス化されているので、プライベート依存(テスト・ダブルに置き換えない)
→Store
クラスにバグがあった場合、Customer
クラスが正しく動作していてもこの単体テストは失敗することになる
# 在庫が十分にある場合、購入は成功する
def test_purchase_succeeds_when_enough_inventory():
# 準備(Arrange)
store = new Store
store.AddInventory(Product.Shampoo,10)
customer = new Customer()
# 実行(Act)
is_success = customer.Purchase(store,Product.Shampoo,5)
# 確認(Assert)
assert success == true
assert store.GetInventory(product.Shampoo) == 5
# 在庫が十分にない場合、購入は失敗する
def test_purchase_fails_when_not_enough_inventory():
# 準備(Arrange)
store = new Store
store.AddInventory(Product.Shampoo,10)
customer = new Customer()
# 実行(Act)
is_success = customer.Purchase(store,Product.Shampoo,15)
# 確認(Assert)
assert success == false
assert store.GetInventory(product.Shampoo) == 10
Product.Shampoo
は協力者オブジェクトではないのか?
Product.Shampoo
は不変オブジェクト(個別の識別性を持たず、自身の内容によってのみ識別されるもの)に分類される。
「自身の内容によってのみ識別される」とは、2つのProduct.Shampoo
があった場合、この2つはお互いに交換して使うことができることを示す。
※数値なども不変オブジェクトに含まれる。
ロンドン学派の場合
-
Store
クラスは不変依存ではないので、テスト・ダブルに置き換える(モックを作成する)
# 在庫が十分にある場合、購入は成功する
def test_purchase_succeeds_when_enough_inventory():
# 準備(Arrange)
store_mock = MagicMock()
def has_enough_inventory_side_effect(product, quantity):
if product == Product.Shampoo and quantity == 5:
return True
return False
store_mock.has_Enough_inventory.side_effect = has_enough_inventory_side_effect
customer = new Customer()
# 実行(Act)
is_success = customer.Purchase(store_mock,Product.Shampoo,5)
# 確認(Assert)
assert success == true
store_mock.RemoveInventory.assert_called_once_with("Shampoo", 5)
# 在庫が十分にない場合、購入は失敗する
def test_purchase_fails_when_not_enough_inventory():
# 準備(Arrange)
store_mock = MagicMock()
def has_enough_inventory_side_effect(product, quantity):
if product == Product.Shampoo and quantity == 5:
return False
return True
store_mock.has_Enough_inventory.side_effect = has_enough_inventory_side_effect
customer = new Customer()
# 実行(Act)
is_success = customer.Purchase(store_mock,Product.Shampoo,5)
# 確認(Assert)
assert success == False
store_mock.RemoveInventory.assert_not_called()
開発方法の違い
- 古典学派:内側から外側に向かうテスト駆動開発
- ロンドン学派:外側から内側に向かうテスト駆動開発
テスト駆動開発とは
テスト駆動開発とは、テストを信頼源としてソフトウェア開発を進める手法のこと
テスト駆動開発の手順
- 追加する機能がどのように振る舞うのかを示すテストケースを作成し、そのテストケースが失敗することを確認する
- 作成したテストケースが成功するのに必要なプロダクションコードを書く
この時点では、そのコードを洗練する必要はない - 実装したコードに対してリファクタリングを行う
既に振る舞いが正しいことを保証するテストケースがあるので、安全にリファクタリングできるようになる
古典学派の考え方
開発方法は内側から外側に向かうテスト駆動開発をする
- 最初にドメイン層から実装とテストを始める
- 上の層の実装とテストを追加する
- 最上層まで実装すると、最終的にエンドユーザが目的のソフトウェアを完全に使えるようになる
※古典学派では、単体テストで協力者オブジェクトに実際のプロダクションコードを使うのでこのような開発方法になる
ロンドン学派の考え方
開発方法は外側から内側に向かうテスト駆動開発をする
- システム全体がどのように機能するのかを考えた広い視野でテストケースを考える
- テスト対象オブジェクトがどの協力者オブジェクトを使うかをモックで明確にして、クラス間の依存を構築する
- テスト対象のプロダクションコードを実装する
協力者オブジェクトを全てモックするので、初めにテスト対象のプロダクションコードを実装できる - 協力者オブジェクトを実装する
統合テストとは
システム全体を検証することでソフトウェアの品質向上に貢献するテスト
古典学派における定義
古典学派における統合テストの定義は、単体テストの定義を1つでも損なっているテスト
(再掲)単体テストの定義
- 「単体 =(1単位の振る舞い)」と呼ばれる少量のコードを検証する
- 実行時間が短い
- (他のテストケースから)隔離された状態で実行される
古典学派における統合テストの例
- 共有依存や実行に時間のかかるプロセス外依存を使ったテスト
- 1つのテストケースの中で1単位の振る舞いを複数検証するテスト
- 異なる振る舞いを検証する遅いテストケースが2つあり、それらのテストケースの手順が似ているので、1つのテストケースにまとめて、2つの異なる振る舞いを検証する場合
(テストケースをまとめた方が効率が良いから) - 異なるチームによって開発された複数のモジュールを統合した後に、その統合したものが意図したように機能するかを検証する場合
- 異なる振る舞いを検証する遅いテストケースが2つあり、それらのテストケースの手順が似ているので、1つのテストケースにまとめて、2つの異なる振る舞いを検証する場合
ロンドン学派における定義
ロンドン学派における統合テストの定義は、実際の協力者オブジェクトを使って行うテスト
E2E(End-to-End)テストとは
E2E(End-to-End)テストとは、エンドユーザの視点でシステムを検証するテスト
テスト対象のアプリケーションが利用するほとんどの外部アプリケーションがE2Eテストでは含まれる
※E2Eテストであっても、全てのプロセス外依存を実際に使えるわけではない
例)外部の決済システム
E2Eテストは、保守の観点において非常にコストのかかるテストなので、全ての単体テストと統合テストが成功するようになってから行う