ソフトウェア開発において、適切なテスト戦略は非常に重要です。
特に、ユニットテストは個々のコンポーネントの信頼性を確保する上で欠かせません。
本記事では、パブリックAPIに注目したテスト設計の重要性と、その限界について解説します。
パブリックAPIとは
まず、パブリックAPIについて理解することが重要です。
APIは「Application Programming Interface」を略しています。
意味としては、ソフトウェアコンポーネント間の相互作用を定義するインターフェースを指します。
パブリックAPIは、そのうち外部(他のクラスやモジュール、時には他のシステム)に公開されているインターフェースのことを指します。
具体的には以下のようなものが含まれます。
- クラスの公開メソッド
- モジュールの公開関数
- ライブラリが提供する公開インターフェース
- Webサービスが提供する外部向けエンドポイント
パブリックAPIは、そのコンポーネントを利用する側(クライアント)が直接アクセスできる部分です。
そして、コンポーネントの機能を利用するための「契約」のようなものと考えることができます。
例えば、以下のPythonクラスを考えてみましょう。
class BankAccount: def __init__(self, initial_balance=0): self._balance = initial_balance def deposit(self, amount): if amount > 0: self._balance += amount return True return False def withdraw(self, amount): if 0 < amount <= self._balance: self._balance -= amount return True return False def get_balance(self): return self._balance def _update_statement(self): # 内部処理のためのメソッド pass
このクラスでは、deposit(), withdraw(), get_balance() メソッドがパブリックAPIに該当します。
これらは外部から直接呼び出すことができ、クラスの主要な機能を提供します。
一方、_balance 属性や _update_statement() メソッドは内部実装の詳細であり、パブリックAPIには含まれません。
パブリックAPIは、コンポーネントの利用者が知る必要がある情報のみを提供し、内部の実装詳細は隠蔽します。
これにより、コードの再利用性が高まり、また内部実装を変更しても外部への影響を最小限に抑えることができます。
パブリックAPIへの注目
ユニットテストを設計する際、パブリックAPIに焦点を当てることが一般的に推奨されています。
これには以下のような利点があります。
- 実装の詳細にとらわれない
- ユーザーが実際に利用するインターフェースをテスト
- コードの重要な動作に集中できる
以下は、パブリックAPIに焦点を当てたテストの例です。
import math class Circle: def __init__(self, radius): self._radius = radius def area(self): return math.pi * self._radius ** 2 def circumference(self): return 2 * math.pi * self._radius # テストコード def test_circle_area(): circle = Circle(5) expected_area = math.pi * 25 assert math.isclose(circle.area(), expected_area, rel_tol=1e-9) def test_circle_circumference(): circle = Circle(5) expected_circumference = 2 * math.pi * 5 assert math.isclose(circle.circumference(), expected_circumference, rel_tol=1e-9)
このテストでは、Circleクラスのパブリックメソッドであるarea()とcircumference()のみをテストしています。
内部実装(_radiusの扱い方など)には言及せず、期待される結果のみを確認しています。
パブリックAPIの限界
しかし、パブリックAPIのみに焦点を当てることには限界があります。
以下のようなケースでは、パブリックAPI以外の要素もテストに含める必要があるかもしれません。
- 外部依存関係の設定が必要な場合
- 副作用の検証が重要な場合
- 内部状態の確認が必要な場合
次の例で、これらの点について詳しく見ていきましょう。
import requests from datetime import datetime class WeatherStation: def __init__(self, api_key): self._api_key = api_key self._last_update = None self._temperature = None def get_temperature(self, city): if self._should_update(): url = f"https://api.weatherservice.com/data?city={city}&key={self._api_key}" response = requests.get(url) data = response.json() self._temperature = data['temperature'] self._last_update = datetime.now() return self._temperature def _should_update(self): if self._last_update is None: return True return (datetime.now() - self._last_update).seconds > 300 # 5分ごとに更新 # テストコード def test_weather_station(mocker): mock_requests = mocker.patch('requests.get') mock_requests.return_value.json.return_value = {'temperature': 25.5} station = WeatherStation('dummy_key') # 最初の呼び出し temp1 = station.get_temperature('Tokyo') assert temp1 == 25.5 mock_requests.assert_called_once() # 5分以内の2回目の呼び出し temp2 = station.get_temperature('Tokyo') assert temp2 == 25.5 mock_requests.assert_called_once() # APIは再度呼ばれていないことを確認 # 5分後の呼び出しをシミュレート mocker.patch('datetime.now', return_value=datetime.now().replace(minute=datetime.now().minute + 5)) station.get_temperature('Tokyo') assert mock_requests.call_count == 2 # APIが再度呼ばれたことを確認
このテストでは、WeatherStationクラスのパブリックメソッドget_temperature()をテストしています。
しかし、同時に以下の点も確認しています。
- 外部API呼び出しのモック化(依存関係の設定)
- 5分以内の再呼び出しでAPIが呼ばれないこと(内部状態の確認)
- 5分経過後の呼び出しでAPIが再度呼ばれること(副作用の検証)
これらの確認は、パブリックAPIだけでなく、クラスの内部動作や外部との相互作用も含んでいます。
まとめ
パブリックAPIに注目したテスト設計は確かに有効ですが、それだけで全てをカバーできるわけではありません。
大切なのは、コードの本当に重要な部分を見逃さずテストすることです。
そのために、次のポイントを意識しましょう。
- できるだけパブリックAPIを使ってテストを組み立てる
- 必要なら、内部の状態や副作用もチェック
- 外部との連携部分は、上手くモック化やスタブ化して再現する
テストの設計は、コードの特徴や求められることに合わせて、臨機応変に考える必要があります。
パブリックAPIへの注目は確かに良いスタート地点です。
ただ、それにこだわりすぎずに、コードの大切な部分をしっかりカバーするテスト方針を考えていきましょう。
上手くテストを設計できれば、コードの質が上がり、バグも早めに見つけられます。
長い目で見れば、プロジェクトの成功につながるはずです。