閉じる

【Python】mockテスト入門:unittest.mockを図解で理解する

Pythonのテストを書くとき、外部APIやDBなどに本物でアクセスすると、テストが遅くなったり不安定になったりします。

こうした問題を解決するために使われるのがmockです。

本記事ではunittest.mockを中心に、図解とサンプルコードで「なぜ」「どう使うか」を体系的に理解できるように丁寧に解説していきます。

Pythonのmockテストとは

mockテストの基本概念と目的

テストでのmockとは、「本物のオブジェクトの代わりに使う、ふるまいだけを再現したテスト専用のダミーオブジェクト」のことです。

Pythonでは主にunittest.mockモジュールを使って実現します。

通常のテストでは、テスト対象のコードが次のような外部要素に依存していることが多いです。

  • 外部Web API
  • データベース
  • ファイルシステム
  • メール送信サービス など

これらに本物でアクセスすると、次のような問題が起こります。

ネットワーク状況に依存してテストが遅くなる、外部サービスの状態によってテストが不安定になる、検証したいパターン(成功・失敗など)を自由に再現しづらいといった問題です。

そこでテスト時だけ本物の代わりにmockを差し込んで動かし、結果だけを検証することで、外部要因に左右されずにテストできるようにします。

テストでmockを使うメリット

mockを使うメリットは多岐にわたりますが、特に重要なポイントを言葉で整理しておきます。

まずテストの高速化です。

外部APIやDBアクセスを実行すると、1つのテストで数秒かかることも珍しくありません。

mockに置き換えれば、単なる関数呼び出しとして数ミリ秒で完了します。

テスト全体の実行時間を桁違いに短縮できます。

次にテストの安定性です。

ネットワーク障害や外部サービスの不具合によって、アプリケーションのバグではないのにテストが落ちる「フレークテスト」が発生しがちです。

mockを使えば、テスト時には外部サービスには一切アクセスしないため、常に同じ結果を得られるテストを作れます。

さらにふるまいの自由な制御という利点もあります。

たとえば外部APIを呼び出す関数に対して、次のようなパターンを簡単に再現できます。

  • 正常なレスポンスを返す
  • エラーコードを返す
  • タイムアウトの例外を発生させる

実際の外部サービスの状態を操作するのは難しいですが、mockならside_effectなどを使って簡単に実現できるため、異常系のテストを書きやすくなります。

このようにmockは、「速く」「安定して」「幅広いケースをカバーできる」テストを書くための強力な道具なのです。

stub・fake・spyとの違い

テスト用に本物のオブジェクトを置き換えるものを総称してテストダブル(test double)と呼び、その中にさまざまな種類があります。

代表的なものを整理しておきます。

stub

stubは「決められた値を返すだけのシンプルなダミー」です。

呼び出し回数や引数にはあまり関心がなく、とにかくテストを進めるために決まった戻り値を返すことが目的です。

fake

fakeは「本物に近い、簡易実装を持つ代替オブジェクト」です。

例えばインメモリ実装のリポジトリや、実際のDBの代わりにPythonのリストでデータを管理するクラスなどです。

機能は簡略化されていますが、本物に近いふるまいを持つため統合テストに向きます。

spy

spyは「実際に処理を行いつつ、呼び出し回数や引数を記録するオブジェクト」です。

戻り値を変える必要はなく、「この関数が本当に呼ばれたか」「何回呼ばれたか」を検証する目的で使われます。

mock

広義のmockは「期待される呼び出し(引数・回数など)を事前に設定し、それに合致したかどうかを検証するテストダブル」です。

Pythonのunittest.mock.Mockは、stubのように値を返し、spyのように呼び出しを記録し、さらにassertで検証までできる総合パッケージだと考えると理解しやすいです。

unittest.mockの基本機能

MagicMockとMockの違い

Pythonのunittest.mockではMockMagicMockがよく登場します。

両者の違いを整理しておきます。

Mockは「ふつうのメソッドや属性」を主に扱う基本的なクラスです。

呼び出し回数の検証やreturn_valueなどの設定ができます。

一方でMagicMockはMockを継承し、特殊メソッド(__len__、__iter__、__enter__など)も最初からmockできるように拡張したクラスです。

たとえばlen(mock_obj)を呼んだり、コンテキストマネージャとしてwith mock_obj:のように使う場合にはMagicMockが便利です。

通常は特に理由がなければMagicMockを使うと考えて問題ありません。

標準ライブラリでも、多くのヘルパー関数はMagicMockを返すようになっています。

簡単なコード例

Python
from unittest.mock import Mock, MagicMock

# 通常のメソッドだけであれば Mock でも十分
user_service = Mock()
user_service.get_user.return_value = {"id": 1, "name": "Alice"}

print(user_service.get_user(1))

# 特殊メソッドを使いたい場合は MagicMock が便利
numbers = MagicMock()
numbers.__len__.return_value = 3

print(len(numbers))
実行結果
{'id': 1, 'name': 'Alice'}
3

return_valueとside_effectの使い方

mockで最もよく使うのがreturn_valueside_effectです。

どちらも「関数を呼んだときに何を返すか(あるいは何を起こすか)」を定義しますが、役割が少し異なります。

return_value: 常に同じ値を返す

return_valueは、そのmockが呼ばれたときに常に同じ値を返すための属性です。

Python
from unittest.mock import MagicMock

# MagicMock を関数のように扱う
get_price = MagicMock()
get_price.return_value = 100  # いつ呼んでも 100 を返す

print(get_price("item-1"))
print(get_price("item-2"))
実行結果
100
100

side_effect: より柔軟なふるまいを定義する

side_effectは、次の3つの使い方ができます。

  1. 関数を指定して、引数に応じて戻り値を変える
  2. 例外クラスやインスタンスを指定して、呼び出し時に例外を送出する
  3. シーケンス(リストなど)を指定して、呼び出しごとに順番に値を返す
Python
from unittest.mock import MagicMock

# 1. 関数を指定
def calc_price(item_id):
    if item_id == "vip":
        return 50
    return 100

get_price = MagicMock()
get_price.side_effect = calc_price

print(get_price("vip"))      # 50
print(get_price("normal"))   # 100

# 2. 例外を送出
fail_api = MagicMock()
fail_api.side_effect = RuntimeError("API error")

try:
    fail_api()
except RuntimeError as e:
    print("Error:", e)

# 3. シーケンスを指定
seq_api = MagicMock()
seq_api.side_effect = [1, 2, 3]

print(seq_api())  # 1
print(seq_api())  # 2
print(seq_api())  # 3
実行結果
50
100
Error: API error
1
2
3

「単に決まった値を返すだけならreturn_value」「引数によって変えたい・例外や連続した戻り値を表現したいならside_effect」と覚えると使い分けやすいです。

call_argsで呼び出しを検証する

mockは「呼び出されたかどうか」「どの引数で呼び出されたか」を記録するspyとしての機能も持っています。

代表的な属性がcall_argsです。

Python
from unittest.mock import MagicMock

send_email = MagicMock()

# テスト対象コードを想定
send_email("user@example.com", subject="Hello", body="Hi!")

print(send_email.call_count)
print(send_email.call_args)
print(send_email.call_args.args)
print(send_email.call_args.kwargs)
実行結果
1
call('user@example.com', subject='Hello', body='Hi!')
('user@example.com',)
{'subject': 'Hello', 'body': 'Hi!'}

さらにassert_*メソッドを使うことで、テストコードとして「こう呼ばれているはずだ」と検証できます。

Python
from unittest.mock import MagicMock

send_email = MagicMock()

send_email("user@example.com", subject="Hello")

# 呼び出しを検証
send_email.assert_called_once()  # 1回だけ呼ばれたか
send_email.assert_called_with(
    "user@example.com",
    subject="Hello",
)

このように、mockは戻り値だけでなく「呼ばれ方」もテストの一部として検証できる点が重要です。

patchで関数やクラスを一時的に差し替える

実際のテストでは、単にmockオブジェクトを作るだけではなく、既存の関数やクラスを「一時的にmockに置き換える」必要があります

このとき使うのがunittest.mock.patchです。

patchは指定した対象(関数・クラス・属性)をテスト中だけ差し替え、終わったら元に戻してくれる便利な関数です。

もっとも基本的な使い方は次のようになります。

Python
# app.py テスト対象コード
import external_api

def get_user_name(user_id: int) -> str:
    user = external_api.get_user(user_id)
    return user["name"]
Python
# test_app.py テストコード
from unittest import TestCase
from unittest.mock import patch, MagicMock

import app

class TestGetUserName(TestCase):
    def test_get_user_name(self):
        # app モジュールが参照している external_api.get_user を差し替える
        with patch("app.external_api.get_user") as mock_get_user:
            mock_get_user.return_value = {"id": 1, "name": "Alice"}

            result = app.get_user_name(1)

            self.assertEqual(result, "Alice")
            mock_get_user.assert_called_once_with(1)
実行結果
(出力は特にないためテストが成功すればOK)

ここではpatch("app.external_api.get_user")のように、「どこから参照しているか(インポート先)」を文字列で指定する点が重要です。

この話は後半の「つまずきポイント」で詳しく解説します。

unittest.mockの具体的な使い方

関数の外部API呼び出しをmockする

ここでは、外部Web APIを呼び出す関数を例に、mockによるテストの流れを見ていきます。

Python
# api_client.py テスト対象コード
import requests

BASE_URL = "https://api.example.com"


def fetch_user_profile(user_id: int) -> dict:
    """外部APIからユーザープロファイルを取得する関数"""
    url = f"{BASE_URL}/users/{user_id}"
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    return response.json()

この関数をテストしたい場合、実際にHTTPリクエストを飛ばすと遅く、不安定です。

そこでrequests.getをmockに差し替えてテストします。

Python
# test_api_client.py
import json
from unittest import TestCase
from unittest.mock import patch, MagicMock

import api_client


class TestFetchUserProfile(TestCase):
    def test_fetch_user_profile_success(self):
        # patch の対象は「api_client.requests.get」
        with patch("api_client.requests.get") as mock_get:
            # モックレスポンスオブジェクトを用意
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"id": 1, "name": "Alice"}
            mock_response.raise_for_status.return_value = None

            mock_get.return_value = mock_response

            result = api_client.fetch_user_profile(1)

            self.assertEqual(result, {"id": 1, "name": "Alice"})
            mock_get.assert_called_once_with(
                "https://api.example.com/users/1",
                timeout=5,
            )

    def test_fetch_user_profile_error(self):
        with patch("api_client.requests.get") as mock_get:
            mock_response = MagicMock()
            # raise_for_status が例外を投げるように設定
            mock_response.raise_for_status.side_effect = Exception("404 Not Found")
            mock_get.return_value = mock_response

            with self.assertRaises(Exception):
                api_client.fetch_user_profile(999)
実行結果
(テストが通ればOK。HTTPリクエストは一度も実行されない)

このようにHTTPライブラリの戻り値オブジェクト自体をMagicMockで作り、必要なメソッドだけ設定するのが実務でもよく使われるパターンです。

クラスメソッド・インスタンスメソッドをpatchする

クラス内部で外部サービスを呼び出している場合、そのメソッドだけをpatchすることもできます。

Python
# payment.py テスト対象コード
class GatewayClient:
    def charge(self, user_id: int, amount: int) -> str:
        """外部決済ゲートウェイを呼び出す(ここはmockしたい)"""
        # ここでは本物の外部サービスを呼ぶと仮定
        raise NotImplementedError


class PaymentService:
    def __init__(self, client: GatewayClient):
        self.client = client

    def pay(self, user_id: int, amount: int) -> str:
        result = self.client.charge(user_id, amount)
        return f"success:{result}"

このGatewayClient.chargeをmockしたテストは次のようになります。

Python
# test_payment.py
from unittest import TestCase
from unittest.mock import patch, MagicMock

from payment import PaymentService, GatewayClient


class TestPaymentService(TestCase):
    def test_pay(self):
        client = GatewayClient()

        # インスタンスメソッド charge を patch.object で差し替える
        with patch.object(client, "charge") as mock_charge:
            mock_charge.return_value = "tx-123"

            service = PaymentService(client)
            result = service.pay(user_id=1, amount=1000)

            self.assertEqual(result, "success:tx-123")
            mock_charge.assert_called_once_with(1, 1000)
実行結果
(テスト実行で外部決済ゲートウェイは呼ばれない)

クラスメソッドやスタティックメソッドも同様にpatch.objectで差し替えられます。

既存のインスタンスに対して差し替えたいときはpatch.objectが便利です。

コンテキストマネージャ(with)でpatchを使う

これまでの例でも使っていますが、with patch(...):の形で使うと、そのブロックの間だけ一時的に差し替えられ、ブロックを抜けると自動的に元に戻るため安全です。

Python
from unittest import TestCase
from unittest.mock import patch

import api_client


class TestWithPatch(TestCase):
    def test_with_patch(self):
        print("before:", api_client.requests)

        with patch("api_client.requests") as mock_requests:
            print("in with:", api_client.requests is mock_requests)
            # ここでは api_client.requests は mock_requests になっている

        print("after:", api_client.requests)
実行結果
before: <module 'requests' from '...'>
in with: True
after: <module 'requests' from '...'>

このように「テストが終わったあとに実装を戻し忘れる」といった事故を防げるので、patchを使うときはコンテキストマネージャ形式を基本にするとよいです。

デコレータでpatchを使う

patchデコレータとしても利用でき、テストメソッドの引数としてmockが渡されます

複数のpatchを重ねる場合にもよく使われるスタイルです。

Python
from unittest import TestCase
from unittest.mock import patch

import api_client


class TestDecoratorPatch(TestCase):
    @patch("api_client.requests.get")
    def test_fetch_user_profile(self, mock_get):
        mock_get.return_value.json.return_value = {"id": 1, "name": "Bob"}
        mock_get.return_value.raise_for_status.return_value = None

        result = api_client.fetch_user_profile(1)

        self.assertEqual(result["name"], "Bob")
        mock_get.assert_called_once()
実行結果
(テスト成功時は出力なし)

デコレータ形式はテストコードがすっきりしやすい反面、複数のpatchを重ねたときに「引数の順序が内側のデコレータから外側へ逆順になる」点には注意が必要です。

この点は後ほど再度図解します。

複数のpatchを組み合わせる

複雑な処理では、同じテスト内で複数の対象をpatchしたいことがよくあります。

その場合の書き方を3パターン示します。

1. withをネストする

Python
with patch("module_a.func1") as mock_func1:
    with patch("module_a.func2") as mock_func2:
        ...

ネストが深くなりやすいため、2つまでなら許容、それ以上は別の方法を検討すると読みやすくなります。

2. withで並列に書く

Python
from unittest.mock import patch

with patch("module_a.func1") as mock_func1, \
     patch("module_a.func2") as mock_func2:
    ...

1行が長くなりがちな点に注意は必要ですが、Python 3系でよく使われる実務的な書き方です。

3. デコレータを複数つける

Python
from unittest import TestCase
from unittest.mock import patch


class TestMultiPatch(TestCase):
    @patch("module_a.func1")
    @patch("module_a.func2")
    def test_something(self, mock_func2, mock_func1):
        # 引数の順番に注意:
        # 一番内側の @patch("module_a.func2") が最初の引数 mock_func2
        # その次の @patch("module_a.func1") が次の引数 mock_func1
        ...
実行結果
(動作の出力は特に想定しない)

デコレータは「上から下に適用され、テスト関数の引数には下から上の順に渡される」というルールを覚えておくと混乱しづらくなります。

属性アクセス・メソッドチェーンをmockする

実務のコードでは、obj.method().attribute.method2()のようなメソッドチェーンや属性チェーンがよく登場します。

MagicMockは「アクセスされた属性やメソッドが、必要に応じて自動的に新しいmockになる」ため、これを活用するとチェーンの途中だけを柔軟に設定できます。

Python
from unittest import TestCase
from unittest.mock import MagicMock


class TestMethodChain(TestCase):
    def test_method_chain(self):
        session = MagicMock()

        # session.query(User).filter_by(id=1).one() が "user-obj" を返すようにしたい
        session.query.return_value.filter_by.return_value.one.return_value = "user-obj"

        result = session.query("User").filter_by(id=1).one()

        self.assertEqual(result, "user-obj")

        # 中間呼び出しも検証可能
        session.query.assert_called_once_with("User")
        session.query.return_value.filter_by.assert_called_once_with(id=1)
実行結果
(テストが成功すればOK)

「途中のメソッドが返すオブジェクトもMagicMockである」と考えると理解しやすいです。

Django ORMやSQLAlchemyなどのORMをmockするときによく使われます。

よくあるつまずきポイントとベストプラクティス

patch対象の指定場所(インポート先)に注意する

unittest.mockで最も多いつまずきが「patchする対象の指定を間違える」ことです。

重要なのは、「定義されている場所」ではなく「テスト対象コードがインポートした場所」をpatchするというルールです。

Python
# external.py
def func():
    return "real"
Python
# module_a.py
from external import func

def use_func():
    return func()

このとき、use_funcをテストしながらfuncをmockしたい場合、次のどちらをpatchすべきでしょうか。

  • patch("external.func")
  • patch("module_a.func")

正解はpatch("module_a.func")です。

なぜなら、module_a.use_funcexternal.funcではなくmodule_a.funcという名前を使って呼び出しているからです。

Python
# test_module_a.py
from unittest import TestCase
from unittest.mock import patch

import module_a


class TestUseFunc(TestCase):
    def test_use_func(self):
        # external.func ではなく module_a.func を patch する
        with patch("module_a.func") as mock_func:
            mock_func.return_value = "mocked"

            result = module_a.use_func()

            self.assertEqual(result, "mocked")
            mock_func.assert_called_once()
実行結果
(テスト成功でOK)

「patchは定義元ではなく“インポート先”を指定する」というルールを頭に入れておくと、mockが効かない原因を減らせます。

過度なmockを避ける設計のコツ

mockは非常に便利ですが、何でもかんでもmockすればよいわけではありません

過度なmockはテストを複雑にし、実装の変更に弱いテストになりがちです。

mockが増えすぎる典型的なパターンは、1つの関数やクラスが次のように多くの外部要素に直接依存している場合です。

  • ネットワーク/API
  • DB
  • ファイルIO
  • 時刻
  • 環境変数 など

こうした場合は、設計レベルで「テストしやすい形」に分割することが重要です。

たとえば次のような工夫が有効です。

  • 外部依存は「インターフェース」「サービスクラス」に閉じ込めておき、そのクラスごとmockする
  • 時刻取得やランダム値などは専用のラッパー関数にし、そこだけをpatchする
  • ビジネスロジックと外部IOを分離し、ロジック部分はmockなしでテスト可能にする

「mockが書けないから設計を変える」のではなく、「テスト・mockをシンプルに書けるような設計を目指す」という発想が大切です。

テストが読みやすいmockの書き方

mockを多用するとテストコードが読みにくくなりがちです。

ここでは、読みやすいmockの書き方のポイントをいくつか挙げます。

まず、「何をテストしたいのか」がmock設定に埋もれないようにすることが大切です。

具体的には次のような工夫が有効です。

  • 複雑なmock設定はヘルパー関数やfixtureに切り出す
  • mockの役割がわかる名前をつける(payment_gateway_mockなど)
  • return_valueside_effectの設定は、テストの「前提条件」としてまとめて書く
  • 呼び出し検証(assert_called_withなど)は、テストの「検証」パートとして最後にまとめる

簡単な良い例を示します。

Python
# 悪い例: 何をしたいテストなのか読み取りづらい
def test_process_order_bad(self):
    with patch("order.requests.post") as m1:
        m2 = MagicMock()
        m1.return_value = m2
        m2.status_code = 200
        m2.json.return_value = {"ok": True}
        # ここからさらに処理と検証が続く...
Python
# 良い例: 前提条件・実行・検証が分かれていて読みやすい
def test_process_order_good(self):
    with patch("order.requests.post") as payment_api:
        self._setup_payment_api_success(payment_api)

        result = order.process_order(order_id=1)

        self.assertTrue(result.is_success)
        payment_api.assert_called_once()

def _setup_payment_api_success(self, payment_api_mock):
    """決済APIが成功するパターンのmock設定"""
    response = MagicMock()
    response.status_code = 200
    response.json.return_value = {"ok": True}
    payment_api_mock.return_value = response
実行結果
(実行結果の出力は特になし)

このようにmockの設定自体を再利用可能な「テスト用シナリオ」として切り出すと、複数のテストで同じ前提条件を簡単に共有でき、テストの意図も明確になります。

unittest.mockで押さえておきたい頻出レシピ

最後に、実務でよく使うunittest.mockの頻出レシピをいくつか紹介します。

現在時刻(datetime.now)をmockする

Python
from unittest import TestCase
from unittest.mock import patch
from datetime import datetime

import my_module


class TestTime(TestCase):
    @patch("my_module.datetime")
    def test_now(self, mock_datetime):
        # datetime.now() が固定の日時を返すようにする
        mock_datetime.now.return_value = datetime(2024, 1, 1, 0, 0, 0)

        result = my_module.get_now_string()

        self.assertEqual(result, "2024-01-01 00:00:00")
実行結果
(テスト成功でOK)

ここでも「patch対象はdatetimeの定義元ではなく、my_moduleがインポートしたdatetime」である点に注意が必要です。

環境変数(os.environ)を一時的に変更する

Python
from unittest import TestCase
from unittest.mock import patch

import my_config


class TestEnv(TestCase):
    @patch.dict("os.environ", {"APP_MODE": "test"}, clear=True)
    def test_env(self):
        # テスト中は APP_MODE が "test" に見える
        self.assertEqual(my_config.get_app_mode(), "test")
実行結果
(テスト成功でOK)

patch.dictは辞書型を一時的に書き換えるのに便利で、環境変数のテストに頻用されます。

ファイルオープン(open)をmockする

Python
from unittest import TestCase
from unittest.mock import mock_open, patch

import file_loader


class TestFileLoader(TestCase):
    def test_read_file(self):
        m = mock_open(read_data="hello")

        with patch("file_loader.open", m):
            result = file_loader.read_file("dummy.txt")

        self.assertEqual(result, "hello")
実行結果
(テスト成功でOK)

mock_openファイルを開く処理を安全にテストするための専用ヘルパーで、コンテキストマネージャとしても動作します。

ランダム値(random.random)を固定する

Python
from unittest import TestCase
from unittest.mock import patch

import lottery


class TestLottery(TestCase):
    @patch("lottery.random.random")
    def test_lottery_win(self, mock_random):
        mock_random.return_value = 0.0  # 必ず当たりになる値

        result = lottery.is_win()

        self.assertTrue(result)
実行結果
(テスト成功でOK)

ランダムな値を返す関数をpatchすることで、確率的な処理も決定的なテストとして書けるようになります。

まとめ

mockはPythonのテストを高速かつ安定させるための、非常に重要なテクニックです。

特にunittest.mockは、MagicMockやpatch、return_valueやside_effectといった仕組みにより、外部APIやDBなどさまざまな依存を簡単に置き換えられます。

本記事で紹介した「インポート先をpatchする」「過度なmockを避ける設計」「読みやすいmockの書き方」「頻出レシピ」を押さえておけば、実務レベルのテストでも十分に活用できます。

まずは小さな関数の外部依存からmockを試し、徐々に自分のプロジェクトに合ったパターンを身につけていきましょう。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!