【Python】モックを使ったテストの書き方(unittest.mock)

Python

こんにちは、かつコーチです。

外部APIを呼び出す処理のテストを書こうとして、こんな悩みに直面したことはありませんか。

「テストのたびに本物のAPIを叩くと、料金がかかるし遅い」

「ネットワークが切れているとテストが落ちる」

この記事では、こうした悩みを解決するモックの使い方を、unittest.mockを中心に解説します。

patchの基本的な使い方から、外部API呼び出しをモック化する実例まで、実践的な内容です。

読み終える頃には、外部依存を切り離した安定したテストが書けるようになります。

モックとは何か

モックの定義

モックとは、本物のオブジェクトの代わりに使う「偽物」のことです。

外部APIやデータベースなど、テストのたびに呼び出すと時間やコストがかかる処理を、モックに置き換えます。

モックは「呼ばれたら決まった値を返す」ように設定でき、本物を呼ばずにテストを完結させられます。

なぜモックが必要なのか

外部APIをそのまま呼び出すテストには、次のような問題があります。

  • 通信環境によってテスト結果が変わる(不安定)
  • API側の料金が発生する場合がある
  • API側が障害中だと、自分のコードが正しくてもテストが失敗する

モックを使えば、これらの外部要因を排除し、自分のコードのロジックだけを検証できます。

patchの使い方

手順1:テスト対象の関数を用意する

外部APIから天気情報を取得する関数を例にします。

# weather.py
import requests


def get_temperature(city):
    """指定した都市の気温を取得する"""
    response = requests.get(f"https://api.example.com/weather/{city}")
    data = response.json()
    return data["temperature"]

手順2:unittest.mock.patchでrequests.getを置き換える

unittest.mock.patchを使うと、requests.getを一時的に別の動きへ差し替えられます。

# test_weather.py
from unittest.mock import patch, MagicMock
from weather import get_temperature


@patch("weather.requests.get")
def test_get_temperature(mock_get):
    # requests.get()の戻り値をモック化する
    mock_response = MagicMock()
    mock_response.json.return_value = {"temperature": 25}
    mock_get.return_value = mock_response

    result = get_temperature("tokyo")

    assert result == 25
    mock_get.assert_called_once_with("https://api.example.com/weather/tokyo")
pytest test_weather.py -v
test_weather.py::test_get_temperature PASSED                           [100%]

@patch("weather.requests.get")のポイントは、weatherモジュールの中で使われているrequests.getを狙って指定していることです。

mock_get.assert_called_once_with(...)で、正しいURLで1回だけ呼ばれたかまで検証できます。

手順3:例外が発生するケースもモック化する

APIがエラーを返すケースも、モックなら簡単に再現できます。

@patch("weather.requests.get")
def test_get_temperature_api_error(mock_get):
    mock_get.side_effect = ConnectionError("接続に失敗しました")

    import pytest
    with pytest.raises(ConnectionError):
        get_temperature("tokyo")

side_effectに例外を渡すと、その関数が呼ばれた瞬間に例外を発生させられます。

本物のAPIをわざと落とすことなく、異常系のテストが書けるのがモックの強みです。

つまずきやすい設定・注意点

patchの第一引数には、「モジュールが実際にimportしている場所」を指定する必要があります。

weather.pyimport requestsとしているなら、パス指定は"weather.requests.get"です。

"requests.get"のように元のライブラリ名で指定すると、意図した箇所が差し替わりません。

よくあるつまずきポイント・エラー対処

patchの指定パスを間違えてモックが効かない

筆者が実際にハマったのが、patchの対象パスを元のライブラリ名で書いてしまったケースです。

❌ Before(モックが効かない書き方)

from unittest.mock import patch, MagicMock
from weather import get_temperature


@patch("requests.get")  # weather.pyの名前空間を指定していない
def test_get_temperature(mock_get):
    mock_response = MagicMock()
    mock_response.json.return_value = {"temperature": 25}
    mock_get.return_value = mock_response

    result = get_temperature("tokyo")
    assert result == 25

実行すると、本物のrequests.getが呼ばれてしまい、次のようなエラーが出ました。

requests.exceptions.ConnectionError: Failed to establish a new connection

モックを設定したはずなのに、実際のネットワーク接続が発生しています。

これは、weather.py側ではweatherモジュールの名前空間の中にあるrequests.getが呼ばれているのに、patch("requests.get")が別の場所を差し替えてしまっているためです。

✅ After(正しい書き方)

from unittest.mock import patch, MagicMock
from weather import get_temperature


@patch("weather.requests.get")  # weather.py内での参照先を指定
def test_get_temperature(mock_get):
    mock_response = MagicMock()
    mock_response.json.return_value = {"temperature": 25}
    mock_get.return_value = mock_response

    result = get_temperature("tokyo")
    assert result == 25

patchのパスは「ライブラリの本来の置き場所」ではなく、「使っているモジュール側から見た参照名」で指定するのが鉄則です。

このルールを覚えてからは、モックが効かないというトラブルはほとんどなくなりました。

まとめ

この記事のポイント

  • モックは、外部APIなどを本物の代わりに使う「偽物」のオブジェクト
  • unittest.mock.patchで、対象の関数やメソッドを一時的に置き換えられる
  • return_valueで戻り値、side_effectで例外の発生を再現できる
  • patchのパスは「使っているモジュール側から見た参照名」を指定する
  • 外部依存を切り離すことで、通信環境に左右されない安定したテストになる

次に読むべき記事

  • pytestの基本:テストの書き方入門
  • unittestとpytestの違い・使い分け
  • テストカバレッジを計測する方法(pytest-cov)

タグ: Python, 上級者向け, テスト

タイトルとURLをコピーしました