はじめに
前編ではPytestの基本・pytest.raises・parametrize・カバレッジを扱いました。
この記事では、少し難しいけど覚えると一気にテストが書きやすくなる2つのテクニックを解説します。
- fixture → テストの「前準備」を共通化する仕組み
- モック → 外部への依存を「ニセモノ」に差し替える仕組み
どちらも「なぜ必要か」から丁寧に説明するので、初めての方でも大丈夫です。
fixture とは?
まず「困る場面」を想像する
たとえば、ショッピングカートを管理するクラスがあるとします。
# cart.py
class Cart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def total(self, prices):
return sum(prices[item] for item in self.items)
このクラスに対して複数のテストを書くと、こうなります:
# test_cart.py
def test_add_item():
cart = Cart() # ← 毎回書く
cart.add("apple")
assert "apple" in cart.items
def test_total():
cart = Cart() # ← 毎回書く
cart.add("apple")
cart.add("banana")
prices = {"apple": 100, "banana": 150}
assert cart.total(prices) == 250
def test_empty_cart():
cart = Cart() # ← 毎回書く
assert cart.items == []
cart = Cart() が毎回登場していて冗長です。テストが増えるほど、この繰り返しが積み重なります。
fixture で解決する
@pytest.fixture を使うと、前準備を1か所にまとめられます。
# test_cart.py
import pytest
from cart import Cart
@pytest.fixture
def cart():
return Cart() # ← ここに前準備をまとめる
def test_add_item(cart): # 引数に fixture 名を書くだけで使える
cart.add("apple")
assert "apple" in cart.items
def test_total(cart):
cart.add("apple")
cart.add("banana")
prices = {"apple": 100, "banana": 150}
assert cart.total(prices) == 250
def test_empty_cart(cart):
assert cart.items == []
ポイント:
-
@pytest.fixtureを付けた関数を定義する - テスト関数の引数に fixture 名(
cart)を書くと、自動で注入される - 各テストは独立した
Cart()インスタンスを受け取る(テスト間で状態が混ざらない)
fixture に後片付けも書ける
データベース接続など「使い終わったら閉じたい」ものは yield で書きます。
@pytest.fixture
def db_connection():
conn = connect_to_db() # 前準備
yield conn # ← テストにこれを渡す
conn.close() # テスト終了後に後片付け
yield の前が「前準備」、後ろが「後片付け」です。
fixture のスコープ
デフォルトでは各テスト関数ごとに fixture が作り直されます。重い前準備(DBの初期化など)を毎回やりたくない場合はスコープを指定できます。
@pytest.fixture(scope="module") # このファイル内で1回だけ実行
def heavy_setup():
...
| スコープ | タイミング |
|---|---|
function(デフォルト) |
テスト関数ごと |
class |
テストクラスごと |
module |
テストファイルごと |
session |
pytest 実行全体で1回 |
モック とは?
まず「困る場面」を想像する
たとえば、天気予報APIを叩いて結果を返す関数があるとします。
# weather.py
import requests
def get_weather(city):
response = requests.get(f"https://api.example.com/weather?city={city}")
data = response.json()
return data["condition"]
この関数のテストを書こうとすると、困ります:
- テストのたびに外部APIへの通信が発生する
- APIが落ちているとテストも失敗してしまう
- テスト環境からインターネットに出られないこともある
- APIの返す値が変わるとテストが不安定になる
テストしたいのは「自分のコードのロジック」であって、外部APIの動作ではない。 そこでモックの出番です。
モックで解決する
unittest.mock の patch を使って、requests.get をニセモノに差し替えます。
# test_weather.py
from unittest.mock import patch, MagicMock
from weather import get_weather
def test_get_weather():
# requests.get の返り値をニセモノにする
mock_response = MagicMock()
mock_response.json.return_value = {"condition": "sunny"}
with patch("weather.requests.get", return_value=mock_response):
result = get_weather("Tokyo")
assert result == "sunny"
何が起きているか:
-
patch("weather.requests.get", ...)→weather.pyの中で使われているrequests.getをニセモノに差し替える -
MagicMock()→ 何でも受け付けるニセオブジェクト。.json()が呼ばれたら{"condition": "sunny"}を返すよう設定 -
withブロックを抜けると元に戻る
実際の通信は一切発生せず、テストが安定して速く動きます。
patch のデコレータ版
with の代わりにデコレータとして書くこともできます。好みで使い分けてください。
from unittest.mock import patch, MagicMock
@patch("weather.requests.get")
def test_get_weather(mock_get):
mock_response = MagicMock()
mock_response.json.return_value = {"condition": "rainy"}
mock_get.return_value = mock_response
result = get_weather("Osaka")
assert result == "rainy"
モックが呼ばれたか確認する
「この関数は1回だけ呼ばれるはず」という検証もできます。
mock_get.assert_called_once() # 1回だけ呼ばれた
mock_get.assert_called_once_with("https://api.example.com/weather?city=Tokyo") # 実際に渡されるURLを指定する
mock_get.assert_called() # 1回以上呼ばれた
mock_get.assert_not_called() # 呼ばれていない
どこをモックすべきか?
よくある迷いポイントです。
| 差し替える対象 | 使うパス |
|---|---|
weather.py 内の requests.get
|
"weather.requests.get" |
weather.py 内の requests モジュール |
"weather.requests" |
「モックするのは、テスト対象のファイルからみたパス」 が鉄則です。requests 本体のパスではなく、weather.py がインポートしている場所を指定します。
fixture × モックの組み合わせ
実際のコードでは fixture の中でモックを設定することもよくあります。
import pytest
from unittest.mock import patch, MagicMock
from weather import get_weather
@pytest.fixture
def mock_weather_api():
mock_response = MagicMock()
mock_response.json.return_value = {"condition": "cloudy"}
with patch("weather.requests.get", return_value=mock_response) as mock:
yield mock
def test_get_weather_cloudy(mock_weather_api):
result = get_weather("Sapporo")
assert result == "cloudy"
fixture の yield で with patch(...) を組み合わせると、テスト終了時に自動でパッチが元に戻ります。
まとめ
| 概念 | 一言で言うと | 使いどころ |
|---|---|---|
| fixture | テストの前準備を共通化する仕組み | 毎回同じセットアップが必要なとき |
| モック | 外部依存をニセモノに差し替える仕組み | API・DB・ファイルI/Oを含む関数をテストするとき |
fixture は「準備の共通化」、モックは「外部依存の切り離し」と覚えておけば使いどころに迷いません。
前編・後編のまとめ
| トピック | 記事 |
|---|---|
| Pytestの基本・assert・失敗の見かた | 前編 |
| pytest.raises・parametrize・カバレッジ | 前編 |
| fixture・モック | 後編(本記事) |
さらに先へ
-
conftest.py→ fixture を複数ファイルで共有する -
pytest-mock→mockerfixture でよりシンプルにモックを書く - CI連携 → GitHub ActionsでPushのたびに自動テストを走らせる