前エントリ「テスタビリティ(試験性)を確保するための設計方針 - 千里霧中」の補足として、「テスタビリティ(試験性、Testability)を拡張可能にする」の実装について解説します。
拡張を実現する手段の一つに接合部(Seam)があります。今回は実装例として、接合部の一種である、メソッドインジェクションを使ってテスタビリティを拡張可能にする例を示します。
対象コード
次のtarget_method()をテストする場合について考えます。
target_method()は、device_controller_run()を呼び出して、外部デバイスを制御するメソッドです。
def device_controller_run(status): ...副作用を持つコード... def target_method(): ... device_controller_run(DeviceStatus.RUNNING) ...
テスタビリティを拡張可能にする
このtarget_method()をテスタビリティ拡張可能にします。その実現手段に、今回は次のようにメソッドインジェクションを導入します。
class DeviceController: def run(self, status): ...副作用を持つコード... def target_method(device_controller): ... device_controller.run(DeviceStatus.RUNNING) ...
メソッドインジェクションは、対象の依存要素を、メソッド呼び出し時に注入するイディオムです。上記では、メソッドの引数としてDeviceControllerオブジェクトを渡し、それ経由で外部デバイス制御メソッドを呼び出すように変更しています。
メソッドインジェクションのような接合部を設けると、接合部を起点にして、後付けでテスタビリティを拡張できるようになります。
テスタビリティを拡張してユニットテストを実現する
ここで、target_method()に対するユニットテストを記述します。今回のユニットテストでは、target_method()が、適切な引数で外部デバイス制御メソッドを呼び出していることを確認します。
最初のtarget_method()の実装例では、このユニットテストを実装できるかは不明です。しかし、前述のようにメソッドインジェクションの導入でテスタビリティを拡張可能にしておくと、次のようなアプローチでテスタビリティを拡張して、ユニットテストを実現できるようになります。
class DeviceController: def run(self, status): ...副作用を持つコード... class DeviceControllerWithTestability(DeviceController): def __init__(self): self._status = DeviceStatus.STOP def run(self, status): self._status = status def status_is_running(self): return self._status == DeviceStatus.RUNNING def target_method(device_controller): '''テスト対象''' ... device_controller.run(DeviceStatus.RUNNING) ... def test_target_method(): '''ユニットテスト''' device_controller = DeviceControllerWithTestability() target_method(device_controller) assert device_controller.status_is_running()
上記では、テスト対象target_method()が依存するDeviceControllerを、DeviceControllerWithTestabilityに置換し、必要なテスタビリティを拡張しています。
テスタビリティの拡張ポイントは、DeviceControllerの副作用を解消する点、(やや過剰ですが)外部デバイス制御メソッドの引数をチェックするAssertionMethodを提供する点となります。
参考文献
接合部を含む、テスタビリティを拡張可能にするイディオムやパターンの解説としては、以下の書籍が充実しています。