ユニットテストは、コードの品質を保証する上で欠かせません。
しかし、外部システムに依存するコードをテストする場合、テストの再現性や安定性が損なわれる可能性があります。
この問題を解決するための手法の一つが、スタブの使用です。
本記事では、スタブを使用したユニットテストの方法について、具体的なコード例を交えて解説します。
外部依存を持つコードをどのようにテストするか、そしてスタブの使用によってどのような利点があるかを見ていきましょう。
外部依存を持つコードの問題点
以下は、外部の在庫管理システムに依存するInventoryManagerクラスの例です。
from abc import ABC, abstractmethod class InventorySystem(ABC): @abstractmethod def get_stock(self, product_id: str) -> int: pass class InventoryManager: def __init__(self, inventory_system: InventorySystem): self.inventory_system = inventory_system def check_stock(self, product_id: str, quantity: int) -> bool: available_stock = self.inventory_system.get_stock(product_id) return available_stock >= quantity # 実際の在庫システムの実装例 class RealInventorySystem(InventorySystem): def get_stock(self, product_id: str) -> int: # 実際のデータベースやAPIから在庫情報を取得する処理 # この例ではダミーの値を返します return 100 # 本来はproduct_idに基づいて適切な在庫数を返す
このコードでは、InventoryManagerクラスがInventorySystemインターフェースに依存しています。
check_stockメソッドは、実際に在庫を確認し、要求された数量が利用可能かどうかを返します。
このようなコードをテストする際の問題点は以下の通りです。
- 実際の在庫管理システムを使用すると、テストのたびに実際のデータが変更される可能性がある。
- 外部システムの状態によってテスト結果が変化する可能性がある。
- テストの実行速度が遅くなる。
スタブを使用したテスト
これらの問題を解決するために、スタブを使用してテストを行うことができます。
スタブは、外部依存のインターフェースをシミュレートし、定義済みの値を返す機能を提供します。
以下は、InventoryManagerクラスをテストするためのコード例です。
import unittest from unittest.mock import Mock class TestInventoryManager(unittest.TestCase): def test_check_stock_sufficient(self): # スタブオブジェクトを作成 stub_inventory_system = Mock(spec=InventorySystem) # スタブの振る舞いを定義 stub_inventory_system.get_stock.return_value = 100 # InventoryManagerのインスタンスを作成 manager = InventoryManager(stub_inventory_system) # テスト実行 result = manager.check_stock("PROD001", 50) # アサーション self.assertTrue(result) stub_inventory_system.get_stock.assert_called_once_with("PROD001") def test_check_stock_insufficient(self): stub_inventory_system = Mock(spec=InventorySystem) stub_inventory_system.get_stock.return_value = 30 manager = InventoryManager(stub_inventory_system) result = manager.check_stock("PROD002", 50) self.assertFalse(result) stub_inventory_system.get_stock.assert_called_once_with("PROD002") if __name__ == "__main__": unittest.main()
このテストコードでは、以下のような特徴があります。
- スタブオブジェクトを使用して、InventorySystemをシミュレートしています。
- stub_inventory_system.get_stock.return_valueを設定することで、スタブオブジェクトの振る舞いを定義しています。
- Mock(spec=InventorySystem)を使用して、スタブがInventorySystemインターフェースに準拠していることを保証しています。
スタブを使用するメリット
スタブを使用してテストを書くことで、以下のようなメリットが得られます。
- 外部システムに依存せずにテストを実行できる。
- テストの再現性が高まり、安定したテスト結果が得られる。
- テストの実行速度が向上する。
- 様々なシナリオ(在庫十分、在庫不足など)を簡単にシミュレートできる。
スタブとモックの違い
スタブとモックは似ているようで異なる概念です。主な違いは以下の通りです。
目的:
- スタブ:特定のメソッド呼び出しに対して、事前に定義された応答を返すことが目的。
- モック:メソッド呼び出しの検証が主な目的。呼び出された回数や引数なども検証できる。
使用方法:
- スタブ:テスト対象のコードに必要な値を提供するために使用。
- モック:テスト対象のコードが期待通りに外部コンポーネントと相互作用しているかを検証するために使用。
検証:
- スタブ:通常、スタブ自体の呼び出しは検証しない。
- モック:モックオブジェクトの呼び出しを検証することが一般的。
例えば、先程のテストコードをより純粋なスタブの使用例に修正すると以下のようになります。
import unittest from unittest.mock import Mock class TestInventoryManager(unittest.TestCase): def test_check_stock_sufficient(self): # スタブオブジェクトを作成 stub_inventory_system = Mock(spec=InventorySystem) # スタブの振る舞いを定義 stub_inventory_system.get_stock.return_value = 100 # InventoryManagerのインスタンスを作成 manager = InventoryManager(stub_inventory_system) # テスト実行 result = manager.check_stock("PROD001", 50) # アサーション self.assertTrue(result) # スタブの呼び出し自体は検証しない def test_check_stock_insufficient(self): stub_inventory_system = Mock(spec=InventorySystem) stub_inventory_system.get_stock.return_value = 30 manager = InventoryManager(stub_inventory_system) result = manager.check_stock("PROD002", 50) self.assertFalse(result) # スタブの呼び出し自体は検証しない if __name__ == "__main__": unittest.main()
この例では、スタブオブジェクトの呼び出し自体は検証せず、テスト対象のメソッドの結果のみを検証しています。
注意点
スタブを使用する際は、以下の点に注意が必要です。
- スタブが実際のシステムの動作を正確に反映していることを確認する。
- 過度にスタブを使用すると、テストが実際の動作から乖離する可能性がある。
- 結合テストやシステムテストなど、他の種類のテストと組み合わせて使用する。
まとめ
スタブを使用したユニットテストは、外部依存を持つコードをテストする際に非常に有効な手法です。
スタブを適切に使用することで、テストの再現性、安定性、速度を向上させることができます。
ただし、スタブの使用には注意点もあります。
実際のシステムの動作を正確に反映させること、そして他の種類のテストと組み合わせて使用することが重要です。
適切にスタブを活用することで、より信頼性の高いテストを書くことができ、結果としてコードの品質向上につながるでしょう。