皆さんは、pytestでunittest.mockを活用したテストを実装した際に、ローカル環境では意図通りに動作するのに、テストスイート全体を実行すると突然失敗する、あるいはテストの実行順序によって成功と失敗が入れ替わるといった不可解な経験をされたことはないでしょうか。
このような問題の根本原因は、実に多くの場合、モックのスコープに対する理解の不十分さに帰結します。
pytestにおけるモックは、関数単位、クラス単位、モジュール単位、さらにはセッション単位まで、その有効範囲を柔軟に制御できます。
しかし、このスコープを適切に設計しないと、異なるテストケース間でモックが意図せず共有されたり、パッチが適用されるタイミングが前後したりすることで、テストの再現性が著しく損なわれます。
特に、フィクスチャのスコープとモックのライフサイクルを一致させない場合、テスト間の依存関係が暗黙的に生じ、結果として「デバッグするたびに挙動が変わる」という最も厄介な状況に陥ります。
モック戦略を立てるにあたり、私は以下の3つの原則を常に重視しています。
- パッチの対象は「インポート時の名前解決先」ではなく「実行時に参照されるオブジェクト」であることを意識し、常にターゲットの所在を正確に指定すること
- フィクスチャのスコープ(function, class, module, session)と、そのフィクスチャが生成するモックの状態保持期間を論理的に整合させること
autouseフィクスチャや組み込みのmockerプラグインが持つ暗黙的な挙動を把握し、副作用を予測可能な範囲に制限すること
例えば、スコープをfunctionに設定せずにモックを生成するフィクスチャを定義すると、複数のテストメソッドが同一のモックインスタンスを参照し続け、呼び出し履歴や戻り値の状態がリセットされません。
このような状態の持ち越しは、単体テストの独立性を破壊する最悪のアンチパターンであり、発見が遅れるほど修正コストが膨らみます。
| スコープ | 有効期間 | 推奨されるユースケース | 注意点 |
|---|---|---|---|
| function | 各テスト関数の実行中 | ほとんどの単体テスト、外部API呼び出しのスタブ | デフォルトで最も安全、明示的に指定推奨 |
| class | テストクラス内の全メソッド | クラス単位で共通の設定や依存が存在する場合 | メソッド間でモック状態が共有される点を留意 |
| module | モジュール全体のテスト実行中 | モジュールレベルの定数やグローバル設定に依存する場合 | モジュール内のテスト順序に注意が必要 |
| session | pytest実行プロセス全体 | システム全体で共通のモック(稀に使用) | ほぼすべてのケースで避けるべき、デバッグが極めて困難 |
ベストプラクティスとして、私は以下のアプローチを強く推奨します。
まず、モックは可能な限り狭いスコープ(function)で定義し、フィクスチャとして明示的に返却する。
次に、デコレータ形式の@patchよりも、mocker.patchを用いたコンテキストマネージャやmockerフィクスチャを活用し、パッチの適用と破棄をコード上で可視化する。
そして、モックの戻り値や副作用は各テストケース内で明示的に設定し、デフォルトの挙動に依存しないことです。
これらの原則を徹底すれば、テストの順序依存性や予期せぬスタブ呼び出しによるエラーは劇的に減少します。
本記事では、各スコープの具体的な動作差異をコード例とともに詳細に検証し、保守性が高く、かつデバッグが容易なテストコードを構築するための実践的なノウハウを体系化して解説します。
あなたのテストが「なぜか通らない」という不毛なデバッグに費やす時間を、大幅に削減できることを確信しています。
pytestのモックでテストが不安定になる根本原因とは

pytestを用いたテストスイートにおいて、モックが絡むテストが突然失敗する、あるいは特定の環境や実行順序でのみ失敗するという経験は、開発者のデバッグ工数を大きく浪費する代表的な要因です。
この不安定性の根源は、モックオブジェクトが持つ状態がテストケースの境界を超えて意図せず共有されるという、スコープ設計の基礎的な見落としにあります。
多くの開発者は、モックを単なる「外部依存の代替物」として捉えがちですが、実態は呼び出し履歴や戻り値、副作用といった内部状態を持つ完全なオブジェクトであり、そのライフサイクルを適切に管理しなければ、テストの再現性は著しく損なわれます。
モックの状態がテスト間で引き継がれるメカニズム
unittest.mockが生成するモックオブジェクトは、call_count、call_args、call_args_list、さらにside_effectやreturn_valueといった属性を保持します。
これらの属性は、モックが呼び出されるたびに更新され、リセットされない限りそのまま蓄積され続けるという特性を持ちます。
問題は、pytestのフィクスチャや@patchデコレータのスコープが適切に設定されていない場合、同一のモックインスタンスが複数のテスト関数で共有されてしまう点にあります。
例えば、スコープをmoduleやsessionに設定したフィクスチャでモックを生成すると、そのモックはモジュール全体またはテストセッション全体で単一のインスタンスとなります。
すると、最初のテストでそのモックを呼び出した際のcall_countが1になり、次のテストではその値がリセットされずに2、3と増加し続けます。
この状態で各テストがassert mock.call_count == 1のようなアサーションを行うと、最初のテストだけが成功し、後続のテストはすべて失敗するという、極めて直感的でない挙動を引き起こします。
さらに厄介なのは、side_effectにイテレータや例外のリストを設定した場合です。
モックが共有されると、イテレータの進行状態も共有されるため、テストAで2回呼び出してイテレータを消費し、テストBで再度呼び出すと想定外のStopIterationが発生する、または想定とは異なる戻り値が返るといった事態に陥ります。
これらの問題は、reset_mock()を明示的に呼び出せば回避可能ではありますが、そのような防御コードを各テストの前後に入れることは、本質的な解決策ではなく、むしろバグの温床となります。
実行順序によって結果が変わる問題の再現例
上記の状態共有が実際にテスト実行順序に依存した失敗を生む具体例を示します。
次のような2つのテスト関数を考えてみましょう。
ここでは、moduleスコープのフィクスチャで共有モックを生成していると仮定します。
import pytest
from unittest.mock import Mock
@pytest.fixture(scope="module")
def shared_mock():
return Mock()
def test_first_call(shared_mock):
shared_mock()
assert shared_mock.call_count == 1
def test_second_call(shared_mock):
assert shared_mock.call_count == 0
このテストスイートにおいて、pytestはデフォルトでテスト関数を名前のアルファベット順に収集・実行します。
そのため、test_first_call(f)はtest_second_call(s)よりも先に実行され、結果としてtest_first_callは成功し、test_second_callはcall_countが1であるためにアサーションに失敗します。
しかし、もしテストファイル内で関数名をtest_a_second_callとtest_b_first_callのように変更すると、実行順序が入れ替わり、今度はtest_a_second_callが先に実行されてcall_countが0のままで成功し、test_b_first_callが後で呼び出してcall_countが1になって成功する、というように成功と失敗が完全に入れ替わる現象が発生します。
| 実行順序(先→後) | test_first_callの結果 | test_second_callの結果 | 全体の成否 |
|---|---|---|---|
| test_first_call → test_second_call | 成功(call_count=1) | 失敗(期待0、実際1) | 失敗 |
| test_second_call → test_first_call | 失敗(期待1、実際0) | 成功(call_count=0) | 失敗 |
どちらの順序でも全体としては失敗しますが、どちらのテストが失敗するかが順序に依存するため、デバッグ時に「さっきまで通っていたテストが今は落ちる」という混乱を招きます。
しかも、テストファイル名やクラス名、マーク付けによって収集順序が変わるため、開発環境とCI環境で挙動が異なることも珍しくありません。
このような非決定的な障害は、まさにモックのスコープ設計が誤っていることの明確なシグナルであり、個々のテストケースのロジックをいくら精査しても解決には至らない、システミックな問題であることを認識すべきです。
unittest.mockのパッチが適用されるタイミングを理解する

モックのスコープ問題と並んで、テストの成否を左右するもう一つの重要な要素が、パッチが実際に適用されるタイミングです。
unittest.mock.patchは、指定されたオブジェクトをテスト実行中に一時的に置き換える強力な仕組みですが、その適用開始点と終了点を誤って認識していると、テストコード上では正しくパッチしているつもりでも、実際には目的のオブジェクトが置き換わっていない、あるいは予期せぬタイミングでパッチが解除されるという事態が生じます。
この問題を理解するには、デコレータ形式とコンテキストマネージャ形式の動作の違い、そしてPythonのインポートシステムがパッチ対象の名前解決に与える影響を正確に把握することが不可欠です。
デコレータとコンテキストマネージャの違い
@patchデコレータは、テスト関数の実行直前にパッチを開始し、関数が戻った直後にパッチを破棄するという明確なライフサイクルを持ちます。
デコレータが複数積み重ねられた場合、パッチは内側から外側の順序で適用され、関数終了時に逆順で解除されます。
この動作は関数単位でスコープが閉じているため、関数内のすべての処理がパッチ対象となりますが、関数内でさらにコンテキストマネージャを使って同じ対象をパッチしようとすると、予期しない競合を引き起こす可能性がある点に留意が必要です。
一方、with patch(...) as mockというコンテキストマネージャ形式は、withブロックの開始時にパッチが適用され、ブロックを抜ける時点で確実に解除されます。
このため、パッチを適用する範囲をコード上の特定の区間に限定でき、関数全体ではなく一部の処理だけをモック化したい場合に柔軟に対応できます。
また、コンテキストマネージャはネストさせることができ、その場合も内側から順に適用され、逆順で解除される点はデコレータと同様です。
| 形式 | 適用開始タイミング | 解除タイミング | スコープの制御性 | 可読性 |
|---|---|---|---|---|
| @patchデコレータ | 関数エントリ直前 | 関数終了直後 | 関数全体に固定 | 宣言的で簡潔 |
| with patchコンテキスト | withブロック開始時 | withブロック終了時 | コードの任意区間 | 明示的で柔軟 |
重要なのは、デコレータとコンテキストマネージャは組み合わせて使用できるという点です。
例えば、クラスメソッド全体にはデコレータでパッチを当て、そのメソッド内の特定部分だけ追加でオーバーライドしたい場合にはコンテキストマネージャを用いるといった使い分けが有効です。
ただし、同一オブジェクトに対してデコレータとコンテキストマネージャが重複してパッチを適用すると、解除順序によっては意図しないモックが残る可能性があるため、二重パッチは原則として避けるべきでしょう。
どちらの方式を選ぶにせよ、パッチの適用範囲と期間を意識して設計することが、不安定なテストを防ぐ第一歩です。
インポート時の名前解決と実行時の参照先の違い
unittest.mock.patchで最も誤解されやすいポイントは、パッチのターゲットとして指定する文字列がインポート時の名前解決に基づいているということです。
Pythonのモジュールはインポートされる際に、そのモジュール内の名前が解決され、オブジェクトがバインドされます。
patchは、指定された「ドット区切りのパス」を使って、そのパスが指す名前がバインドされている場所を実行時に探索し、その場所に格納されたオブジェクトを置き換えます。
ここで致命的な誤解が生じます。
例えば、myapp.utilsモジュールにExternalAPIクラスがあり、myapp.serviceがfrom myapp.utils import ExternalAPIとインポートしている場合を考えましょう。
このとき、patch('myapp.utils.ExternalAPI')はmyapp.utilsモジュールの名前空間にあるExternalAPIを置き換えますが、myapp.serviceモジュール内でローカルにバインドされたExternalAPI(つまりmyapp.service.ExternalAPI)は置き換わりません。
なぜなら、from ... import文はインポート時にオブジェクトをモジュールの属性としてコピーするため、元のモジュールの属性を変更しても、既にコピーされた参照は影響を受けないからです。
したがって、正しくパッチを当てるには、パッチ対象を「使用する側」の名前空間で指定する必要があります。
先の例では、patch('myapp.service.ExternalAPI')とすべきです。
このルールは、テストコード内でインポートする場合も同様で、テストモジュールがfrom myapp.service import Serviceとしているなら、patch('myapp.service.ExternalAPI')を指定しなければ、Service内部で参照されるExternalAPIは差し替わりません。
| インポート形式 | パッチ指定例 | 実際に置き換わる名前空間 |
|---|---|---|
import myapp.utils でアクセス |
patch('myapp.utils.ExternalAPI') |
myapp.utils.ExternalAPI |
from myapp.utils import ExternalAPI |
patch('myapp.service.ExternalAPI') (使用先で指定) |
myapp.service.ExternalAPI |
この動作は、Pythonの属性解決の仕組み(スコープチェーンではなく、オブジェクトが所属するモジュールの辞書に基づく)に起因しており、多くの開発者が「モックが効かない」と悩む原因の大半を占めます。
パッチは、対象のオブジェクトが定義されている場所ではなく、それが参照されている場所に対して適用するという原則を常に意識してください。
この原則を守るだけで、デコレータとコンテキストマネージャの選択と相まって、パッチの適用タイミングに関するミスは劇的に減少するはずです。
pytestが提供するモックスコープの全種類と公式ガイドライン

pytestのフィクスチャシステムは、スコープを指定することで、オブジェクトの生成タイミングと生存期間を細かく制御できるよう設計されています。
このスコープの概念はモックにもそのまま適用され、@pytest.fixtureのscope引数にfunction、class、module、sessionのいずれかを渡すことで、モックインスタンスがどの範囲で共有されるかを決定できます。
公式ドキュメントでは、原則として最も狭いスコープをデフォルトに採用し、必要が生じた場合のみ広げるというガイドラインが示されています。
この原則を軽視すると、テストの独立性が損なわれ、デバッグ困難な障害に直結するため、各スコープの特性を正確に理解することが成功への近道です。
functionスコープの特性とデフォルト動作
pytestのフィクスチャは、scopeを明示しない場合、デフォルトでfunctionスコープとして動作します。
これは、各テスト関数の実行ごとにフィクスチャが新規生成され、関数終了時に破棄されることを意味します。
モックにおいては、各テストケースが完全に独立したモックインスタンスを受け取るため、call_countやcall_argsといった状態が他のテストに漏れることは決してありません。
この特性は、単体テストの最も重要な性質である「テスト間の独立性」を担保する上で理想的です。
実際の実装では、以下のようにフィクスチャを定義することで、各テストにフレッシュなモックを供給できます。
import pytest
from unittest.mock import Mock
@pytest.fixture
def fresh_mock():
return Mock()
このfresh_mockはscope指定がないためfunctionとなり、テスト関数ごとに新たなMockインスタンスが生成されます。
このスコープは、外部APIの呼び出しをスタブ化する場合や、データベースアクセスを模倣する場合など、ほとんどの単体テストシナリオにおいて最も安全かつ推奨される選択肢です。
また、mockerフィクスチャ(pytest-mock提供)もデフォルトでfunctionスコープで動作し、各テスト終了時に自動的にパッチを解除するため、特に理由がなければこのデフォルトに従うべきでしょう。
classスコープが有効なケースと危険性
scope="class"を指定すると、フィクスチャは同一テストクラス内のすべてのテストメソッドで単一のインスタンスを共有します。
このスコープが有効なケースとしては、クラス全体で共通の設定や依存オブジェクトが必要であり、かつそのオブジェクトがテストメソッド間で状態を変更しないことが静的に保証されている場合に限られます。
例えば、読み取り専用の設定値や、不変のスタブデータを提供するフィクスチャなどが該当します。
しかし、モックに関しては、このスコープは大きな危険性をはらみます。
なぜなら、モックは本質的に呼び出し履歴や副作用といった可変状態を持つからです。
クラス内のメソッドAがモックを呼び出してcall_countをインクリメントし、続けてメソッドBがそのモックを検証するようなテストを書いた場合、メソッドBはメソッドAの呼び出し履歴を引き継いでしまいます。
これを回避するには、各メソッドの前後にreset_mock()を呼び出すなどの事前処理が必要になりますが、そのような防御コードはスコープ設計自体が誤っていることを示す明白なシグナルです。
私の経験則では、classスコープのモックはほぼすべてのケースで避けるべきであり、どうしても必要な場合は、モックではなく実際の軽量なスタブオブジェクト(状態を持たないダミー)を利用することを検討してください。
それでもclassスコープを用いる場合は、フィクスチャ内でモックの状態をリセットする仕組みを必ず組み込むか、各テストメソッドでmock.reset_mock()を明示的に呼び出すルールをチームで徹底する必要があります。
moduleスコープとsessionスコープの極端な使用例
scope="module"は、フィクスチャがモジュール全体で一度だけ生成され、そのモジュール内のすべてのテスト関数で共有されるスコープです。
scope="session"に至っては、pytestの実行プロセス全体で単一インスタンスが維持され、複数のテストモジュールにまたがって共有されます。
これらのスコープは、極めて例外的な状況でなければ使用すべきではありません。
有効な使用例としては、システム全体で共通の環境変数や、変更されることのない設定オブジェクト、あるいは起動に非常に大きなコストがかかる外部リソース(例:Dockerコンテナの接続プール)のモックが考えられます。
しかし、そのようなリソースであっても、モックに置き換えるのであれば、ほとんどの場合functionスコープで十分であり、パフォーマンス上の理由からmoduleやsessionを選ぶことは早すぎる最適化であることが多いです。
| スコープ | 生成頻度 | 共有範囲 | 推奨度 | 主なリスク |
|---|---|---|---|---|
| function | テスト関数ごと | 単一関数内 | 最高 | リスクほぼなし |
| class | クラスごと | クラス内全メソッド | 低 | 状態共有による順序依存 |
| module | モジュールごと | モジュール内全関数 | 極低 | モジュール間の隠れた結合 |
| session | セッションごと | 全テスト全体 | 禁止に近い | デバッグがほぼ不可能 |
特にsessionスコープは、テストの実行順序だけでなく、テストファイルの収集順序にも依存するため、再現性がほぼ完全に失われます。
CI環境とローカル環境で挙動が異なるのは必定であり、修正にはテスト全体のリファクタリングが必要になることも珍しくありません。
公式ガイドラインでも、sessionスコープの使用は「本当に必要で、かつその影響を完全に理解している場合に限定する」と明記されており、私もこの見解に強く同意します。
モックのスコープは、常に最も狭いfunctionを第一選択とし、それで不足する場合のみ一段階ずつ広げるという慎重なアプローチを採ることをお勧めします。
スコープ不一致が引き起こす代表的な障害パターン

スコープの設計ミスは、一見するとテストロジックのバグと区別がつかない多彩な障害パターンを生み出します。
これらの障害に共通するのは、単一のテストケースを単独で実行すると成功するという不可解な性質です。
ここでは、現場で最も頻繁に遭遇する三つの典型的なパターンを、そのメカニズムとともに解剖していきます。
呼び出し履歴のリセット忘れによる誤ったアサーション
モックの最も基本的な検証手法は、assert_called_once_withやassert_called_with、あるいはcall_countの確認です。
しかし、スコープがclass以上で共有されるモックに対して、テストメソッド内でreset_mock()を呼び出さない場合、前のテストが残した呼び出し履歴がそのまま残り、アサーションが期待通りに評価されなくなります。
具体的には、テストAでモックが2回呼び出された後、テストBでassert mock.call_count == 1と書くと、実際のcall_countは2のままなので失敗します。
さらに厄介なのは、assert_called_once_withは「一度だけ呼び出され、かつ指定された引数で呼び出された」ことを検証しますが、もし前のテストで異なる引数で呼び出されていた場合、call_args_listには複数のエントリが残るため、このアサーションも同様に失敗します。
| 障害パターン | 具体的な症状 | 根本原因 | 推奨対策 |
|---|---|---|---|
| call_countの過剰 | 期待値より大きな数値で失敗 | 前テストの呼び出しが蓄積 | 各テストでreset_mock()、またはfunctionスコープに変更 |
| call_argsの混在 | 期待した引数が履歴の最後にない | 前テストの引数呼び出しが残存 | アサーション前にcall_args_listを確認するか、独立モックを使用 |
| assert_called_once_withの誤検知 | 複数回呼び出しで失敗 | 前テスト分もカウントされる | スコープをfunctionにして確実に1回のみにする |
この問題の本質は、モックの状態がテストの論理的な境界を超えて持続することにあります。
最も確実な解決策は、モックのスコープをfunctionに設定することです。
それができない事情がある場合(例えば、クラス単位でセットアップコストを節約したい場合)は、各テストメソッドの冒頭で必ずmock.reset_mock()を呼び出すルールを徹底してください。
ただし、このルールは忘れがちであり、レビューでも見過ごされやすいため、私は後者の選択肢を推奨しません。
副作用が前のテストの影響を受ける事例
side_effectにイテレータやリストを渡すことで、モックが呼び出されるたびに異なる戻り値や例外を返すように設定できます。
この機能は非常に便利ですが、スコープが共有されている場合、イテレータの消費状態も共有されるため、前のテストがイテレータを進めてしまうと、後のテストでは想定外のStopIterationが発生したり、意図しない順序の値が返されたりします。
例えば、mock.side_effect = [1, 2, 3]と設定し、テストAで2回呼び出して1と2を消費し、テストBで改めて最初から1を得たいと思っても、実際には3が返ってきます。
さらに、side_effectに例外のリストを設定している場合、テストAで例外を発生させた後、テストBでも同じ例外が発生し続けるという、より致命的な事態も起こり得ます。
この問題の厄介な点は、reset_mock()ではside_effect自体は初期化されないことです。
reset_mock()は呼び出し履歴をクリアしますが、side_effectのオブジェクト(イテレータ)はそのまま残るため、イテレータの状態はリセットされません。
したがって、共有スコープでside_effectを用いる場合は、各テストごとにside_effectを再設定するか、そもそもfunctionスコープで新しいモックを生成する方が安全です。
フィクスチャの寿命とモックの寿命のギャップ
pytestのフィクスチャは、スコープに従って生成と破棄が行われますが、モックオブジェクト自体がフィクスチャの内部で生成される場合、そのモックの生存期間はフィクスチャのスコープに従います。
しかし、モックがパッチを適用する対象は、フィクスチャのスコープとは無関係に、Pythonのモジュールやクラスにバインドされています。
このギャップが問題を生むのは、例えばmoduleスコープのフィクスチャでパッチを適用した場合、そのパッチはモジュール全体で有効ですが、フィクスチャがスコープ内で一度だけ生成されると、そのモックインスタンスがずっと保持され続けるためです。
より深刻なのは、フィクスチャがfunctionスコープでも、そのフィクスチャが返すモックが外部のオブジェクトを参照している場合です。
例えば、フィクスチャ内でmock = Mock()を返すだけであれば問題ありませんが、mock = patch('some.module').start()のようにして開始したパッチをフィクスチャの終了時にstop()しないと、パッチ自体はフィクスチャのスコープを超えて残り続けます。
この場合、フィクスチャの寿命は短くても、モックがパッチしたグローバルなオブジェクトは変更されたままになるため、後続のテストに影響を及ぼします。
このギャップを埋めるには、フィクスチャ内でyieldを用いて、パッチの適用と破棄を確実に行うファイナライザを実装するのが定石です。
pytest-mockのmockerフィクスチャはこの処理を自動化してくれるため、私は特に理由がない限りmocker.patchを利用することを推奨します。
これにより、フィクスチャのスコープとモックの有効期間を完全に一致させることができ、上記のようなギャップに起因する障害を未然に防げます。
安全なテスト設計のためのfunctionスコープ徹底活用

これまで見てきたように、モックの状態共有やスコープ不一致が引き起こす障害は、テストの信頼性を根底から損ないます。
これらの問題を根本的に回避するための最も現実的かつ実践的な戦略は、モックを常にfunctionスコープで運用することです。
この方針は、単に「デフォルトだから」という消極的な理由ではなく、テストの独立性、再現性、そしてデバッグ容易性という三つの品質指標を同時に満たすための合理的な選択肢です。
ここでは、その具体的な実装パターンと、それによって得られるメリットを体系的に解説します。
各テストケースでモックを新規生成する実装パターン
functionスコープを徹底するための実装は、極めてシンプルです。
pytestのフィクスチャにscope引数を一切指定しない(デフォルトのfunctionを利用する)か、明示的にscope="function"と記述します。
そして、そのフィクスチャ内でモックオブジェクトを生成して返すか、あるいはpytest-mockプラグインが提供するmockerフィクスチャを直接利用します。
以下に、推奨される二つの実装パターンを示します。
import pytest
from unittest.mock import Mock
# パターン1: フィクスチャで新規モックを生成
@pytest.fixture
def user_mock():
return Mock(name="user_mock")
# パターン2: mockerフィクスチャを利用(pytest-mock必要)
def test_with_mocker(mocker):
mock_api = mocker.patch("myapp.service.ExternalAPI")
mock_api.get_data.return_value = {"status": "ok"}
# このテスト内でのみ有効なモック
パターン1では、テスト関数がuser_mockを要求するたびに、新しいMockインスタンスが生成されます。
これにより、各テストは完全に独立したモックを持ち、call_countやcall_argsが他のテストに影響を与えることは絶対にありません。
パターン2では、mocker.patchがテスト関数の終了時に自動的にパッチを解除し、かつ生成されたモックもそのスコープ内に限定されます。
どちらのパターンも、モックの生成と破棄をテストライフサイクルに完全に同期させる点で優れています。
重要なのは、これらのパターンをテストコード全体で統一することです。
一部のテストでclassスコープやmoduleスコープを混在させると、そのテストだけが特異な振る舞いを示し、チーム全体の認識齟齬を生みます。
私は、プロジェクトのテスト設計ガイドラインとして「モックフィクスチャは必ずfunctionスコープとする」というルールを明文化することを強く推奨します。
フィクスチャをfunctionスコープで定義するメリット
functionスコープの採用がもたらすメリットは、単に「テストが通る」という次元を超えて、テストスイートの保守性と開発生産性に直結します。
以下に、その主要な利点を列挙します。
- テスト間の完全な独立性:各テストが独自のモックインスタンスを持つため、テストの実行順序に依存する障害が根本的に発生しません。CIとローカルで結果がぶれる問題も解消されます
- デバッグの局所性:テストが失敗した場合、その原因はそのテスト関数内のロジックまたはそのテスト専用のモック設定に限定されるため、問題箇所の特定が極めて容易になります。共有モックが絡む「どこで状態が変わったか」という追跡作業から解放されます
- アサーションの信頼性:
assert_called_once_withやcall_countの検証が、そのテスト内での呼び出しだけを対象とするため、期待値と実績値の一致が保証されます。前のテストの残骸を考慮する必要がありません - リファクタリング耐性の向上:新しいテストを追加したり、既存のテストを並べ替えたりしても、他のテストへの影響を一切考慮する必要がありません。テストスイートの拡張が線形に進み、結合度が低く保たれます
- 並列実行との親和性:pytest-xdistなどでテストを並列実行する場合、functionスコープのモックは各ワーカープロセスで独立して生成されるため、競合状態が発生しません。共有スコープでは並列実行時に予期しない競合が頻発しますが、functionスコープではそのリスクがゼロになります
| 評価軸 | functionスコープ | class/module/sessionスコープ |
|---|---|---|
| テスト間独立性 | 完全に独立 | 状態共有による依存が発生 |
| デバッグ容易性 | 局所的な原因特定が可能 | グローバルな状態追跡が必要 |
| 並列実行対応 | 問題なし | 競合リスクが高い |
| アサーションの予測性 | 常にそのテスト内のみ対象 | 前テストの影響を受ける |
| コードの明示性 | 各テストで設定が完結 | セットアップ順序に注意が必要 |
これらのメリットを享受するために、functionスコープは「制約」ではなく「設計上の保証」と捉えるべきです。
たとえセットアップコストがわずかに増えるとしても(実際にはモックの生成は極めて軽量です)、そのコストはテストの安定性と開発者の精神衛生に対して支払うに値する対価です。
私は、あらゆる新規プロジェクトでこの方針を採用し、過去にスコープ問題で苦しんだレガシーテストも、functionスコープへのリファクタリングを優先的に行うことをお勧めします。
そうすることで、テストが「不安定な付属品」から「信頼できる品質の証」へと変貌するはずです。
mockerフィクスチャとpytest-mockを用いたクリーンなモック管理

unittest.mockだけでもパッチ操作は可能ですが、pytest環境ではpytest-mockプラグインが提供するmockerフィクスチャを活用することで、より宣言的でエラーの少ないモック管理を実現できます。
このプラグインは、パッチの自動クリーンアップや、スコープに応じた適切なライフサイクル制御を標準化してくれるため、開発者はモックの「片付け」から解放され、テストロジックそのものに集中できるようになります。
特に、手動でpatch.start()とpatch.stop()を呼び出す煩雑さや、try/finallyブロックで確実に解除するための冗長なコードを書く必要がなくなる点は、大きな生産性向上に寄与します。
mocker.patchによる明示的なパッチ適用と破棄
mocker.patchは、unittest.mock.patchと同様の引数を受け取りながら、テスト関数の終了時に自動的にパッチを解除するという動作を持ちます。
この動作は、mockerフィクスチャがfunctionスコープで動作し、各テストの終了時に内部で保持しているパッチスタックをすべてクリーンアップするからです。
これにより、開発者はパッチの適用を宣言的に記述するだけで、後処理を一切意識する必要がなくなります。
具体的な実装例を見てみましょう。
以下のコードは、外部APIクライアントをモック化して、特定の戻り値を返すように設定するテストです。
def test_fetch_user_data(mocker):
mock_client = mocker.patch("myapp.services.ApiClient.fetch")
mock_client.return_value = {"id": 1, "name": "Taro"}
result = get_user_data(1)
assert result["name"] == "Taro"
mock_client.assert_called_once_with(1)
このテストが終了すると、mockerはmyapp.services.ApiClient.fetchに対するパッチを自動的に解除します。
mocker.patchはデコレータとしても利用可能で、@mocker.patch("...")と記述することもできますが、フィクスチャとしてのmockerを用いることで、コンテキストマネージャのネストに比べて可読性が向上し、かつ複数のパッチを同時に適用する際もスッキリと記述できます。
さらに、mocker.patch.objectやmocker.patch.multipleといったバリエーションも用意されており、クラスメソッドや複数属性の一括パッチも容易です。
これらの関数はすべて、テスト終了時の自動クリーンアップが保証されているため、手動でのstop()呼び出し忘れによるパッチ残存という、従来のpatch.start()方式で頻発していたヒューマンエラーを完全に排除できます。
また、mocker.spyを用いれば、既存のオブジェクトの呼び出しを監視するスパイを作成することもでき、状態変更を伴わない検証が可能になります。
| 機能 | 手動パッチ(start/stop) | mocker.patch |
|---|---|---|
| パッチの明示性 | 開始と停止を別々に記述 | 宣言的に1行で記述 |
| クリーンアップの保証 | 開発者が責任を持つ | テスト終了時に自動実行 |
| 例外時の安全性 | try/finallyが必要 | 自動的に解除される |
| 複数パッチの可読性 | ネストが深くなりがち | フラットに記述可能 |
| スパイ機能 | 別途実装が必要 | mocker.spyで標準提供 |
autouseフィクスチャの濫用を避ける判断基準
autouse=Trueを指定したフィクスチャは、テスト関数が明示的にそれを要求しなくても、自動的に適用されます。
この機能は、全テストに共通のセットアップ(例:環境変数の一時変更、ログレベルの調整)を一括で行う場合に便利ですが、モックに対して適用する際には極めて慎重な判断が求められます。
なぜなら、autouseフィクスチャはテストコード上にその存在が明示されないため、テストの挙動を理解するために暗黙の前提を強いられるからです。
autouseフィクスチャでモックを適用すべき例外的に許容されるケースは、以下の条件をすべて満たす場合に限られます。
- パッチ対象がシステム全体で不変の共通依存(例:
datetime.nowやos.getenv)であり、かつ全テストで同じ振る舞いが期待されること - そのモックが副作用を持たず、呼び出し履歴を検証する必要がないこと(単なるスタブとしての利用)
- テストスイート全体でそのモックの設定が変更されることが絶対にないこと
一方で、以下のような状況ではautouseを決して使用すべきではありません。
- テストごとに異なる戻り値や副作用を設定する必要がある場合
- モックの呼び出し回数や引数を検証するテストが含まれる場合
- 特定のテストクラスやモジュールだけで必要なパッチである場合
- パッチの適用順序がテスト結果に影響を与える可能性がある場合
autouseフィクスチャがもたらす最大のリスクは、テストの可読性と保守性の低下です。
あるテストが失敗したとき、その原因がautouseフィクスチャによる暗黙のモックにあると、開発者はまず「どこでパッチが当たっているのか」を探すために、全フィクスチャ定義をスキャンしなければなりません。
この調査コストは、テストスイートが大きくなるほど非線形に増加します。
私は、autouseフィクスチャをモックに使うことを事実上のアンチパターンとして捉えており、代替案として、明示的なフィクスチャを各テストで要求するか、conftest.pyに定義した通常のフィクスチャを必要なテストだけが明示的に参照する方式を推奨します。
どうしてもautouseを用いる場合は、そのフィクスチャの名前をautouse_で始めるなど、命名で暗黙性を明示化し、かつドキュメントにその影響範囲を詳細に記述することを義務付けるべきです。
結局のところ、テストコードは実行可能性だけでなく、読み手に対する説明責任も負っているという認識を持ち、autouseの使用はその責任を果たすための最後の手段として位置づけることが、成熟したテスト設計の証と言えるでしょう。
テスト順序依存を断ち切るための設計パターンとリファクタリング

テスト実行順序に依存する不安定性は、モックスコープの誤りが最も顕著に表面化する症状ですが、その根本的な治療はテスト設計そのものの見直しにあります。
順序依存が発生するということは、テスト間に暗黙の状態共有が存在する証拠であり、これを取り除くには、モックのライフサイクルを独立化するか、共有すべきデータと状態を明確に区別するかの二択に帰着します。
ここでは、現実的な制約の中で順序依存を確実に排除するための具体的な設計パターンと、既存コードを安全にリファクタリングする手順を提示します。
モックの状態を事前に明示的にリセットする方法
最も即効性のある対症療法は、各テストの実行前にモックの状態を明示的にリセットすることです。
unittest.mockが提供するreset_mock()メソッドは、call_count、call_args、call_args_list、そしてside_effectの進行状態(イテレータの消費状況を除く)をクリアします。
このメソッドをフィクスチャのセットアップ部分やテストメソッドの冒頭で呼び出すことで、共有モックであっても各テストを初期状態から開始できます。
import pytest
from unittest.mock import Mock
@pytest.fixture(scope="class")
def shared_mock():
return Mock()
@pytest.mark.usefixtures("shared_mock")
class TestWithReset:
def test_first(self, shared_mock):
shared_mock.reset_mock() # 明示的なリセット
shared_mock.return_value = 10
assert shared_mock() == 10
assert shared_mock.call_count == 1
def test_second(self, shared_mock):
shared_mock.reset_mock() # 忘れずに呼ぶ
shared_mock.return_value = 20
assert shared_mock() == 20
assert shared_mock.call_count == 1
この方法の利点は、既存のスコープ設計を変更せずに済むことです。
しかし、大きな代償も伴います。
まず、すべてのテストメソッドでreset_mock()を呼び出すことを強制されるため、呼び忘れが発生すると再び順序依存が復活します。
また、side_effectにイテレータを設定している場合、reset_mock()はイテレータの消費状態をリセットしないため、別途イテレータを再生成する必要があります。
さらに、パッチ自体は解除されないため、return_valueやside_effectの設定は各テストで上書きしなければなりません。
このアプローチは、移行期間中の暫定策としては有効ですが、長期的な戦略としてはお勧めしません。
なぜなら、リセットの義務が開発者の記憶に依存する以上、ヒューマンエラーが完全に排除できないからです。
理想的には、リセットを自動化するフィクサチュア(例:autouse=Trueのフィクスチャで各テスト前にリセット)を導入するか、より抜本的にfunctionスコープへ移行することを検討すべきです。
テスト間で共有するデータを最小限にするアプローチ
順序依存を根本から断つには、そもそも共有状態を持たない設計が最も優れています。
functionスコープへの全面移行が理想ですが、既存の大規模テストスイートでは現実的でない場合もあります。
そのような場面では、共有データの範囲を意図的に狭め、モック以外の部分でも状態共有を極力減らす設計パターンが有効です。
第一のパターンは、フィクスチャをファクトリ関数化することです。
共有フィクスチャが単一のインスタンスを返すのではなく、テストが呼び出すたびに新しいインスタンスを生成する関数を返すようにします。
これにより、テストは明示的に「新しいモックが欲しい」と要求でき、スコープを広げる副作用を回避できます。
@pytest.fixture
def mock_factory():
def _create():
return Mock()
return _create
def test_with_factory(mock_factory):
m1 = mock_factory()
m2 = mock_factory()
assert m1 is not m2 # 完全に独立
第二のパターンは、共有すべき不変データと、可変なモック状態を分離することです。
例えば、テスト間で共通の設定値(例:APIエンドポイントURL、タイムアウト秒数)はスコープを広げたフィクスチャで共有し、呼び出し履歴や戻り値といった可変部分は各テストで新規に生成する構成にします。
この場合、共有フィクスチャはモックそのものではなく、設定オブジェクトや定数辞書などを提供し、各テストがそれらを参照して独自のモックを生成します。
| 設計戦略 | 共有対象 | 可変モックの扱い | 順序依存のリスク |
|---|---|---|---|
| 完全functionスコープ | 何も共有しない | 各テストで新規生成 | ゼロ |
| ファクトリフィクスチャ | 生成関数のみ | テストが呼び出して生成 | ほぼゼロ |
| 不変データのみ共有 | 設定値、定数 | 各テストで個別モック | 低い(モックに注意) |
| モック自体を共有(非推奨) | モックインスタンス | リセットが必要 | 高い |
さらに、テストコードのリファクタリングにおいては、テスト間の依存関係を可視化することも重要です。
pytestの--durationsオプションで実行時間を確認したり、pytest-orderプラグインで意図的な順序付けを行うことも可能ですが、それらは対症療法に過ぎません。
真の解決は、各テストが自己完結しており、他のテストの成否に影響されないことを保証する設計にあります。
そのためには、テスト作成時に「このテストは他のテストが先に実行されていることを前提としていないか」と自問する習慣を身につけ、コードレビューでは共有フィクスチャのスコープとモックのリセット有無を必ずチェックポイントに含めることを推奨します。
これらのプラクティスを積み重ねることで、テストスイートは順序に左右されない堅牢なものへと進化していくでしょう。
モックのデバッグを効率化するログ出力と検証テクニック

モックのスコープ設計を適正化しても、テストが期待通りに動作しないことは依然として起こり得ます。
その原因の多くは、モックに対する検証(アサーション)の誤りや、呼び出し履歴の見落としにあります。
モックは「呼び出されたか」「何回呼び出されたか」「どの引数で呼び出されたか」を詳細に記録していますが、これらの情報を効果的に引き出し、解釈するスキルが不足していると、デバッグは暗中模索に陥ります。
ここでは、pytest環境でのモック検証を効率化する具体的なテクニックと、失敗時に有益な情報を自動的に出力する設定方法を体系的に解説します。
assert_called_withやcall_args_listの適切な使い方
unittest.mockは、モックの呼び出しを検証するための豊富なアサーションメソッドを提供しています。
中でも基本中の基本がassert_called_withとassert_called_once_with、そしてcall_args_listの活用です。
しかし、これらの使い方を誤ると、テストが意図しない成功や、逆に意味不明な失敗を報告することがあります。
assert_called_with(*args, **kwargs)は、モックが直近の呼び出しで指定された引数と一致することを検証します。
つまり、過去の呼び出しは無視され、最後の1回だけが検査対象です。
この動作は「最後の呼び出しだけが重要」というシナリオでは有効ですが、複数回の呼び出しを想定する場合には注意が必要です。
例えば、モックが3回呼び出され、最後の呼び出しが期待通りの引数であれば、それ以前の間違った呼び出しは検出されません。
一方、assert_called_once_withは「一度だけ呼び出され、かつその引数が一致すること」を要求します。
このアサーションは、モックが予期しない追加呼び出しを検出するのに非常に強力ですが、もしモックが一度も呼び出されなかったり、複数回呼び出されたりすると、即座に失敗します。
この厳格さが逆に、テストの前提を明確にする効果を持ちます。
より詳細な検証を行うには、call_args_list属性を直接利用します。
これは、モックが受けたすべての呼び出しをcallオブジェクトのリストとして保持しており、各callは位置引数とキーワード引数をタプルとして格納します。
以下の例では、モックが特定の引数シーケンスで呼び出されたことを検証しています。
from unittest.mock import Mock, call
mock = Mock()
mock(1, x=2)
mock(3, y=4)
assert mock.call_args_list == [call(1, x=2), call(3, y=4)]
このようにcall_args_listを用いれば、呼び出し順序や各呼び出しの引数の完全な一致を確実にチェックできます。
また、部分一致を検証したい場合は、リスト内包表記やany()と組み合わせて、特定の呼び出しパターンが存在するかどうかを柔軟に判定することも可能です。
| アサーション手法 | 検証対象 | 複数呼び出し時の挙動 | 推奨ユースケース |
|---|---|---|---|
assert_called_once_with |
一度だけ、かつ引数一致 | 複数回で失敗(厳格) | モックが確実に1回だけ呼ばれるべき場合 |
assert_called_with |
直近1回の引数一致 | 過去の誤りを無視 | 最後の呼び出しだけに関心がある場合 |
call_args_list の等式検証 |
全呼び出しの完全一致 | 順序と引数すべてを比較 | 呼び出し履歴全体を検証したい場合 |
call_args_list の部分一致検索 |
特定の呼び出しパターンの存在 | 柔軟に条件指定 | 「少なくとも1回、特定引数で呼ばれた」ことを確認 |
適切な検証手法を選ぶための指針として、私はデフォルトでassert_called_once_withを使用し、モックが複数回呼び出されることを明示的に期待する場合のみcall_args_listによる検証に切り替えることを推奨します。
これにより、想定外の余分な呼び出しを早期に検出でき、テストの堅牢性が向上します。
モックの呼び出し履歴をテスト失敗時に出力する設定
アサーションが失敗したとき、pytestはデフォルトで簡潔なエラーメッセージを表示しますが、モックの呼び出し履歴に関する詳細は含まれません。
例えば、assert_called_once_withが失敗した場合、「Expected call: mock(1, x=2)」と表示されるものの、実際にどのような呼び出しが行われたかは表示されないため、開発者は手動でデバッガを起動したり、print(mock.call_args_list)を挿入したりする必要があります。
この非効率を解消するために、pytestのフックやカスタムアサーションを活用して、失敗時に自動的に呼び出し履歴を出力する設定を導入することを強くお勧めします。
最も簡単な方法は、pytest.iniまたはconftest.pyで、pytestのpytest_assertrepr_compareフックを実装し、モック関連のアサーションが失敗した場合に追加のコンテキストを表示することです。
ただし、この方法は実装が複雑になりがちです。
より現実的な代替案として、アサーションの前に失敗時のログ出力を明示的に仕込むプラクティスがあります。
def assert_mock_called_once_with(mock, *args, **kwargs):
try:
mock.assert_called_once_with(*args, **kwargs)
except AssertionError as e:
print(f"モックの全呼び出し履歴:\n{mock.call_args_list}")
print(f"現在の call_count: {mock.call_count}")
raise
このようなヘルパー関数をテストユーティリティとして定義し、標準のアサーションの代わりに使用することで、失敗時に即座に履歴情報が得られるようになります。
また、pytestの--capture=noオプションと組み合わせれば、これらの出力がコンソールに確実に表示されます。
さらに、pytest-mockのmockerフィクスチャには、mocker.assert_called_once_withなど、標準のアサーションと同様のメソッドが用意されていますが、失敗時のメッセージがやや詳細になる場合があります。
しかし、本格的なデバッグ情報を得るには、caplogフィクスチャを用いて、モックの呼び出しのたびにログを出力するようにラップすることも有効です。
def test_with_logging(mocker, caplog):
mock = mocker.patch("myapp.service.api_call")
original_call = mock.side_effect
def logged_call(*args, **kwargs):
caplog.info(f"モック呼び出し: args={args}, kwargs={kwargs}")
if original_call:
return original_call(*args, **kwargs)
mock.side_effect = logged_call
# テスト実行後、caplog.textに呼び出し履歴が蓄積される
この手法は、テストが失敗した場合だけでなく、テストが成功しているときでも呼び出しパターンを確認できるため、開発中のデバッグにも非常に有用です。
ただし、過剰なログ出力はテスト実行速度に影響するため、必要に応じて環境変数でオンオフを切り替える仕組みを併用すると良いでしょう。
最終的には、CI環境でもこれらの情報を取得できるように、pytestの--verboseや--show-capture=allオプションをデフォルトで有効にし、さらにpytest.iniにaddopts = --verbose --tb=longを設定することで、失敗トレースバックに可能な限りのコンテキストを含めることを推奨します。
モックの呼び出し履歴は、デバッグの最強の手がかりです。
それを見える化する仕組みをテストスイートに組み込むことで、「なぜモックが期待通りに動かないのか」という謎は、大部分が自己解決するようになるでしょう。
まとめ:スコープを意識したモック戦略でメンテナブルなテストへ

本記事では、pytestとunittest.mockを用いたテストにおいて、モックのスコープ設計がいかにテストの信頼性と保守性に直結するかを、根本原因から障害パターン、そして具体的な実装テクニックに至るまで体系的に解説してきました。
ここで改めて強調したいのは、モックは単なる「外部依存の代用品」ではなく、呼び出し履歴や副作用といった内部状態を持つ第一級のオブジェクトであり、そのライフサイクルをテストケースの境界と整合させることが、再現性のあるテストスイートを構築するための絶対条件だという点です。
数多くのプロジェクトのテストコードをレビューしてきた経験から、私は以下の三つの黄金原則を導き出しました。
- モックのスコープは、常に
functionをデフォルトとし、やむを得ない場合のみ一段階ずつ広げる。class, module, sessionは「禁止」に近い意識を持つ - パッチの適用には
mocker.patch(pytest-mock)を利用し、手動のstart()/stop()やtry/finallyによる後処理は絶対に避ける - モックの検証は
assert_called_once_withやcall_args_listを用いて厳格に行い、失敗時に呼び出し履歴を可視化する仕組みをテストスイートに組み込む
これらの原則を守ることで、テストの実行順序に依存する不可解な失敗はほぼ完全に排除され、デバッグに費やす時間は劇的に短縮されます。
また、テストコードが自己完結的になるため、新たなメンバーが参加してもスコープの罠に悩まされることがなくなり、チーム全体の生産性が向上するという副次的な効果も期待できます。
既存のレガシーテストスイートで、すでにclassやmoduleスコープのモックが散在している場合、一度にすべてをリファクタリングすることは現実的ではありません。
そのような状況では、優先順位をつけて段階的にfunctionスコープへ移行する戦略をお勧めします。
まずは、最も頻繁に失敗するテストや、並列実行で問題を起こしているテストから着手し、修正の都度、スコープを狭めて状態共有を断ち切ります。
同時に、reset_mock()による暫定対応を導入しつつ、長期的にはファクトリフィクスチャやmockerへの置き換えを進めていくことで、無理なく全体の品質を引き上げられます。
| チェック項目 | 推奨状態 | 危険シグナル |
|---|---|---|
| フィクスチャのスコープ | function(明示またはデフォルト) |
class, module, sessionが指定されている |
| パッチの適用方法 | mocker.patch またはコンテキストマネージャ |
patch.start() と patch.stop() の手動呼び出し |
| モックの検証 | assert_called_once_with または call_args_list による完全検証 |
assert_called_with のみに依存(過去の呼び出しを見逃す) |
| テスト間の状態共有 | 一切なし(各テストで新規モック) | 同一モックインスタンスを複数テストで使用している |
| デバッグ支援 | 失敗時に呼び出し履歴を出力する仕組みがある | 失敗時に手動でprint文を追加する必要がある |
最後に、モック戦略は単なるテクニックではなく、テスト設計の根幹を成すアーキテクチャ上の意思決定であるという認識を持っていただきたいと思います。
スコープを適切に設計することは、テストの信頼性を高めるだけでなく、コードの変更に対する安心感をもたらし、結果としてリファクタリングや機能追加のスピードを加速させます。
本記事で紹介したプラクティスを日々の開発に取り入れ、テストが「不安定な足かせ」ではなく「品質を支える強固な基盤」として機能することを願っています。
皆さんのテストスイートが、よりメンテナブルで、より頼りになるものへと進化する一助となれば、これに勝る喜びはありません。


コメント