テストダブル#

テストダブル(test double) はソフトウェアテストにおいて「本物の依存オブジェクトの代わりに使う偽物のオブジェクト」の総称。(名前の由来は「スタントダブル(代役、スタントマン)」から)

本来のコンポーネントを呼び出すとテストが難しくなったり、外部リソース(DB、API、ネットワーク、ファイルシステムなど)に依存して不安定になる場合に、それを置き換えて使う。

テストダブルの種類#

Gerard Meszarosが書籍『xUnit Test Patterns: Refactoring Test Code』で示した分類(Webにも公開されている

  • Stub(スタブ) :呼び出されたときに、あらかじめ決めた値を返す。「入力 → 出力」を固定することで外部依存を切り離す

  • Mock(モック) :期待される呼+び出し(回数や引数)をあらかじめ設定し、それを満たさないとテスト失敗にする。Stubを含むが間接的な出力も検証(assert)する

  • Dummy(ダミー) :渡さないとコンパイルや実行ができないので形だけ用意するもの。実際には使われない(使おうとすると壊れるようにする)。

  • Fake(フェイク) :本物に近いが、実装を簡略化した代替物 (例: 本物はRDBMSだが、テストではインメモリDBを使う)

  • Spy(スパイ) :呼び出し履歴や引数を記録して、後で検証できるようにするもの

MockとStubの違い

  • Stubは 入力に対する出力を固定する

  • Mockは 入力に対する出力を固定する のに加えて、 呼び出し回数や出力のassertを行う 。 システムの中で「どんな引数で、何回呼び出されたか」をチェックしたい意図がある

Pythonのunittest.mockモジュールはスタブとしてもモックとしても使える

スタブとして使う例:

# unittest.mockをスタブとして使う
from unittest.mock import Mock
stub = Mock()
stub.get_data.return_value = "fixed value"  # 返り値固定
print(stub.get_data())  # fixed value
# 呼び出し回数などは気にしない

モックとして使う例:

# unittest.mockをモックとして使う
from unittest.mock import Mock
mock = Mock()
mock.get_data.return_value = "value"
mock.get_data("arg1")

# 呼び出しの回数を検証
mock.get_data.assert_called_once_with("arg1")

スタブの例#

WeatherService というクラスがあり、指定した都市の天気予報を返すとする。

WeatherAPIClient というクラスで天気予報を取得するとする。WebAPIの呼び出しが絡むので、テストのときは実際のAPIに依存しないようにしたい

# --- テスト対象のコード ---
class WeatherAPIClient:
    """都市の天気を取得するAPIのクライアント"""
    def get_weather(self, city: str) -> str:
        # 実際にはHTTPリクエストなどが必要(テストには不向き)
        raise NotImplementedError("実際のAPI呼び出しは未実装")


class WeatherService:
    def __init__(self, api_client: WeatherAPIClient):
        self.api_client = api_client

    def get_forecast_message(self, city: str) -> str:
        weather = self.api_client.get_weather(city)
        return f"The weather in {city} is {weather}."

WeatherAPIStubを作り、決め打ちの値を返すようにする

# --- スタブの実装 ---
class WeatherAPIStub(WeatherAPIClient):
    def get_weather(self, city: str) -> str:
        # 都市ごとに決め打ちの返り値を用意する
        stub_data = {
            "Tokyo": "Sunny",
            "Osaka": "Cloudy",
        }
        return stub_data[city]

# --- テスト ---
def test_weather_service():
    stub_client = WeatherAPIStub()  # ← スタブを注入
    service = WeatherService(api_client=stub_client)

    assert service.get_forecast_message("Tokyo") == "The weather in Tokyo is Sunny."
    assert service.get_forecast_message("Osaka") == "The weather in Osaka is Cloudy."

test_weather_service()

unittestのMagicMockでスタブを書く#

MagicMock を使って既存の関数を上書きし、返り値を固定することができる

参考:unittest.mock - Python documentation

from unittest.mock import MagicMock

stub_client = WeatherAPIClient()
stub_client.get_weather = MagicMock(return_value="Sunny") # 返り値を固定
stub_client.get_weather()
'Sunny'
def test_weather_service():
    service = WeatherService(api_client=stub_client)
    assert service.get_forecast_message("Tokyo") == "The weather in Tokyo is Sunny."

test_weather_service()

unittest.mockでスタブを書く#

from unittest.mock import Mock

def test_weather_service_with_mock():
    # Mockオブジェクトを作成
    api_client_mock = Mock()

    # get_weatherメソッドの返り値を都市ごとに指定
    api_client_mock.get_weather.side_effect = lambda city: {
        "Tokyo": "Sunny",
        "Nagoya": "Rainy"
    }.get(city, "Unknown")

    service = WeatherService(api_client=api_client_mock)

    assert service.get_forecast_message("Tokyo") == "The weather in Tokyo is Sunny."
    assert service.get_forecast_message("Nagoya") == "The weather in Nagoya is Rainy."
    assert service.get_forecast_message("Osaka") == "The weather in Osaka is Unknown."

test_weather_service_with_mock()

モックの例#

unittest.mock にはモックがどう呼び出されたのか検証するメソッドが用意されている

https://docs.python.org/ja/3/library/unittest.mock.html

from unittest.mock import Mock

api_client_mock = Mock()
api_client_mock.get_weather.return_value = "Sunny"

# 呼び出し
api_client_mock.get_weather("Tokyo")

# 呼び出し回数や引数を検証
api_client_mock.get_weather.assert_called()  # 少なくとも一度は呼び出されたことをassertする
api_client_mock.get_weather.assert_called_once()  # 一度だけ呼び出されたことをassertする
api_client_mock.get_weather.assert_called_once_with("Tokyo") # 一度だけ、特定の引数で呼び出されたことをassertする