【第1回】Python pytest 入門 — unittest.mockでモックを使ったテストの基本を押さえよう

プログラミング

テストを書こうとするたびに「外部APIの呼び出しどうすればいいんだ…」と手が止まりがちだったのですが、unittest.mock を使いこなせるようになってからかなりスムーズになりました。この記事ではそのあたりの基本を整理します。

このシリーズ「Python pytest 入門」では、pytest の基本的な使い方から始めて、モック・フィクスチャ・カバレッジ計測まで全4回で扱う予定です。

  • 第1回(今回): unittest.mock でモックを使ったテストの基本
  • 第2回: pytest fixture の使い方と scope の違い
  • 第3回: pytest-mock を使ったより実践的なモックパターン
  • 第4回: カバレッジ計測と CI への組み込み

第1回は unittest.mock の基礎から。pytest と組み合わせた使い方を中心に書いていきます。

この記事でわかること

  • unittest.mock がなぜ必要なのか、モックの基本的な考え方
  • MagicMock の基本的な使い方
  • @patch デコレータで外部依存を差し替える方法
  • side_effect を使った例外やシーケンス制御
  • モックの呼び出しを検証するアサーションメソッド

そもそもモックって何のためにあるのか

テストを書くとき、困るのが「外の世界に依存している処理」です。たとえば:

  • 外部 API を叩く関数
  • データベースに書き込む処理
  • ファイルシステムを操作するコード
  • 現在時刻を返す datetime.now()

こういった処理をそのままテストすると、ネットワーク状況やデータの状態によって結果が変わってしまいます。テストは「同じ条件なら常に同じ結果になる」べきなので、これだと困ります。

そこでモック(Mock)の出番です。モックは「本物の代わりに置く偽物」で、「このAPIを呼んだらこの値を返してほしい」という振る舞いを事前に設定できます。本物の処理は実行されないので、テストが高速になるし、外部依存もなくなる、というわけです。

余談ですが、モックを使いこなすには「テスト対象の境界をどこに引くか」をちゃんと意識する必要があって、それがまたけっこう難しいんですよね。この記事ではそこまで深入りせず、まず動かせることを優先します。

環境準備

pytest は別途インストールが必要ですが、unittest.mock は Python 3.3 以降の標準ライブラリに含まれているので追加インストール不要です。

pip install pytest

動作確認した環境は Python 3.12 / pytest 8.x としています。

MagicMock の基本的な使い方

unittest.mock の中でよく使うのが MagicMock です。まずは単体で触ってみます。

from unittest.mock import MagicMock

mock_func = MagicMock()

# 呼び出すと MagicMock が返ってくる
result = mock_func(42)

# 呼ばれたかどうかを検証
mock_func.assert_called_once_with(42)

# 返り値を指定したいとき
mock_func.return_value = "hello"
print(mock_func())  # "hello"

MagicMock はとにかく何でも受け付けてくれる便利な偽物です。属性アクセスも呼び出しも全部受け流してくれます。

MockMagicMock の違いは、後者がマジックメソッド(__len____iter__ など)もあらかじめ定義している点です。基本的には MagicMock を使っておけば間違いないと思います。

@patch デコレータでテスト対象の依存を差し替える

実際のテストでよく使うのが @patch デコレータです。テスト実行中だけ特定のオブジェクトをモックに差し替えてくれます。

たとえば、こういうコードがあるとします:

# notifier.py
import requests

def notify_slack(message: str) -> bool:
    response = requests.post(
        "https://hooks.slack.com/services/xxx",
        json={"text": message}
    )
    return response.status_code == 200

この関数をテストしたいのに、テストのたびに Slack に通知が飛んでしまっては困ります。@patch を使って requests.post をモックに差し替えます:

# test_notifier.py
from unittest.mock import MagicMock, patch
from notifier import notify_slack

@patch("notifier.requests.post")  # ここ注意: インポート先のパスを指定する
def test_notify_slack_success(mock_post):
    mock_post.return_value.status_code = 200

    result = notify_slack("テスト通知")

    assert result is True
    mock_post.assert_called_once()

ポイントは @patch の引数です。requests.post ではなく notifier.requests.post と書く必要があります。「モックしたいオブジェクトが実際に使われている場所のパス」を指定するのがルールで、ここを間違えるとモックが効きません。最初ここでハマりました。

差し替え先のパスを間違えやすいケース

特に from xxx import yyy でインポートしている場合は注意が必要です。

# notifier_v2.py
from requests import post  # ← こっちでインポートしている場合

def notify_slack(message: str) -> bool:
    response = post("https://hooks.slack.com/services/xxx", json={"text": message})
    return response.status_code == 200

この場合、パッチするのは requests.post ではなく notifier_v2.post になります:

@patch("notifier_v2.post")  # ← from import した場合はこちら
def test_notify_slack_v2(mock_post):
    mock_post.return_value.status_code = 200
    assert notify_slack("テスト") is True

「なんでモックが効かないんだろう」ってなるやつの大半がこれだと思います。

with 構文を使った patch

デコレータの代わりに with 文で書くこともできます。テスト関数の中でモックの有効範囲を限定したいときに便利です。

from unittest.mock import patch
from notifier import notify_slack

def test_notify_slack_with_context():
    with patch("notifier.requests.post") as mock_post:
        mock_post.return_value.status_code = 200
        result = notify_slack("テスト通知")

    assert result is True
    # with ブロックを出た後はモックが解除されている

個人的には、テスト関数が長くなってきたら with 構文のほうが「どこでモックが有効か」が視覚的にわかりやすくて好みです。

side_effect で例外やシーケンスを返す

return_value は「常に同じ値を返す」場合に使いますが、例外を発生させたいときや呼び出しごとに異なる値を返したいときは side_effect を使います。

from unittest.mock import MagicMock, patch
import pytest
from notifier import notify_slack

# 例外を発生させるテスト
@patch("notifier.requests.post")
def test_notify_slack_raises(mock_post):
    mock_post.side_effect = ConnectionError("接続失敗")

    with pytest.raises(ConnectionError):
        notify_slack("テスト通知")

# 呼び出しごとに異なる値を返す
def test_side_effect_sequence():
    mock = MagicMock()
    mock.side_effect = [1, 2, 3]

    assert mock() == 1
    assert mock() == 2
    assert mock() == 3

side_effect にリストを渡すと、呼び出すたびに順番に値が返ってきます。リストを使い切った後に呼び出すと StopIteration が上がるので、必要な分だけ用意しておくのがコツです。

呼び出しを検証する assert メソッド

モックが「正しく呼ばれたか」を検証するアサーション系のメソッドも整理しておきます。

from unittest.mock import MagicMock

mock = MagicMock()
mock("arg1", key="value")

# 一度だけ呼ばれたか
mock.assert_called_once()

# 指定の引数で一度だけ呼ばれたか
mock.assert_called_once_with("arg1", key="value")

# 直近の呼び出しが、指定の引数だったか
mock.assert_called_with("arg1", key="value")

# 一度も呼ばれていないか
mock.assert_not_called()

# 呼ばれた回数を直接確認
assert mock.call_count == 1

これだけ把握しておけばたいていのケースには対応できます。あと mock.call_args_list で全呼び出しの引数履歴が取れるので、複数回呼ばれた場合の詳細検証にも使えます。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

正直、最初は「なんでパスの書き方がこんなにわかりにくいんだ」と思っていましたが、「使われている場所を指定する」という原則を理解してからは迷わなくなりました。モックの仕組み自体はシンプルで、MagicMock + @patch + side_effect の組み合わせが使いこなせれば、実務レベルのほとんどのケースはカバーできると思います。

📚 シリーズ「Python pytest 入門」(第1回 / 全4回)

→ 次回の記事: 公開後にリンクが追加されます

参考になったらクリックしてもらえると嬉しいです!

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