当サイトは、アフィリエイト広告を利用しています

【Python】pytestのよく使うMockの置き換えパターン

作成日:2025月01月21日
更新日:2025年02月18日

Pythonでアプリケーション等の動作確認をpytestを使って行う場合
テスト対象のモジュール以外をmockに置き換えることで効率的に
テスト対象モジュールの動作を確認できる。

当記事では、そのテスト対象モジュール以外の部分を

  • Mockに置き換える方法(monkeypatch)
  • 置き換えのよくあるパターン

ついてまとめておく。

Mockオブジェクト自体の使い方やmockを使うことのメリットについては
下記記事でまとめているので当記事と合わせて参照してほしい

テスト対象のモジュール以外をmockに置き換えるとは?

たとえば、テスト対象モジュール

  • mainモジュール
  • classモジュール

があり、mainモジュールからclassモジュールをよんで処理をしている場合
mainモジュール内でclassモジュールを使っている部分はmockに置き換えて
テストを行う。

こうすることでテスト対象モジュールであるmainモジュールの
テストに集中することができる。

pytest実行の流れ

pythonのpytestでモックを使う場合、基本的には

  • モックオブジェクトを作る
  • モックオブジェクトに任意のメソッドや属性を設定
  • モックオブジェクトを任意のメソッドやクラス、インスタンスと置き換える
  • テスト実行

のような流れで作成して使用する。

今回はこの手順の「モックオブジェクトを任意のメソッドやクラス、インスタンスと置き換える」の部分に
フォーカスしてまとめる。

Mockに置き換える方法

mockへの置き換えはpytestのmonkeypatchを使って行う。

monkeypatchの使い方

monkeypatchは

  • 引数を2個とる場合
  • 引数を3個とる場合

がある。

引数を2個とる場合

引数が2個
# モジュールやクラスを文字列で指定する場合
monkeypatch.setattr("module_path.attribute_name", mock_value)
  • 第1引数に操作対象のパスを文字列で指定する
  • 第2引数には置き換え対象を指定する※通常はここがmockになる

引数を3個とる場合

引数を3個
# クラスやモジュールを直接参照できる場合
monkeypatch.setattr(target_object, "attribute_name", mock_value)
  • 第1引数に操作対象のオブジェクトを指定
  • 第2引数には第1引数に指定したオブジェクト内の属性や関数などを指定
  • 第3引数には置き換え対象を指定する※通常はここがmockになる

置き換えのよくあるパターン

サンプルでいくつかモジュールを作り、
よくあるmockの置き換えパターンをまとめる。

モジュール構成

下記の4つのモジュールを作る

  • main.py
  • ids.py
  • user.py
  • utils.py

各モジュールの関係は下記のようなイメージになる 2025-01-20-20-49-59

main.pyは

  • ids.py
  • user.py
  • utils.py

のクラスや関数をimportして処理をしている
上記の4つもモジュールを編集しながらよく出る置き換えパターンのサンプルをまとめる

外部クラスのインスタンスをmockに置き換えるパターン~その①~

テスト対象のモジュール内で外部クラスをimportし、インスタンス化して
使用している場合の置き換えパターン

user.py

user.py
class UserClass:
def __init__(self,name:str, age:int):
self.name = name
self.age = age
# インスタンスメソッド
def get_user(self):
return self.name, self.age

main.pyからget_userを使う

main.py

main.py
from user import UserClass
def main():
# UserClassインスタンス作成
user_instance = UserClass(name="saikawa", age=23)
# UserClassのインスタンスメソッドを呼び出し
name, age = user_instance.get_user()
return name, age
if __name__ == "__main__":
print(main())

mainメソッドではハイライト部分は UserClassのインスタンスメソッドを使っている。

test_main.py

test_main.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
import main
# テスト対象のクラスでimportしてる外部クラス
from user import UserClass
def test_main_true(monkeypatch):
# インスタンスモックを作成
mock_user_ins = MagicMock(spec=UserClass)
# インスタンスモックのメソッドを設定
mock_user_ins.get_user.return_value = "saionji",11
# クラスのモックを生成
mock_user_class = MagicMock()
# クラスのモックが呼ばれた場合にインスタンスモックが返るようにする
mock_user_class.return_value=mock_user_ins
# クラスモックに置き換える
monkeypatch.setattr('main.UserClass', mock_user_class)
# テスト実行
name, age = main.main()
# 結果検証
assert name == "saionji"
assert age == 11

テストではテスト対象であるmain.pyのmainメソッドの動作を確認するため、
mainメソッドでimportしてインスタンス化して使用してるUserClassをmockに置き換えている。

詳しく解説する

インスタンスモック作成

インスタンスモック作成
# インスタンスモックを作成
mock_user_ins = MagicMock(spec=UserClass)
  • UserClassを引数に設定することで、UserClassを模倣したモックができる。
  • UserClassはインスタンス化して使うのでここではインスタンスのモックを作っている

インスタンスモックの関数設定

インスタンスモックの関数設定
# インスタンスモックのメソッドを設定
mock_user_ins.get_user.return_value = "saionji",11
  • インスタンスモックもメソッド「get_user」の戻り値を設定する

クラスモック作成

クラスモック作成
# クラスのモックを生成
mock_user_class = MagicMock()
  • クラスのmockオブジェクトを作成する
  • この時点では空オブジェクト

クラスモックの戻り値にインスタンスモックを設定

クラスモックの戻り値にインスタンスモックを設定
# クラスのモックが呼ばれた場合にインスタンスモックが返るようにする
mock_user_class.return_value=mock_user_ins
  • クラスモックが呼び出された時に、戻り値としてインスタンスモックを返すように設定する

UserClassとクラスモックを置き換える

UserClassとクラスモックを置き換える
# クラスモックに置き換える
monkeypatch.setattr('main.UserClass', mock_user_class)
  • mainモジュールでimportしているUserClassをクラスモックに置き換える
  • これでクラスからインスタンスを生成する動作をmockで再現することができる

実行

インスタンス生成
# テスト実行
name, age = main.main()

main関数内でUserClassのインスタンス生成処理がモックの置き換わった状態で
テスト実行される

外部クラスのインスタンスをmockに置き換えるパターン~その②~

上記のパターンではmainメソッド内でUserClassのインスタンスを生成していたが
グローバル領域で生成していた場合はそのグローバル変数を置き換える必要がある。

main.py

main.py
from user import UserClass
# グローバル領域でインスタンス生成
user_instance = UserClass(name="saikawa", age=23)
def main():
name, age = user_instance.get_user()
return name, age

test_main.py

test_main.py
import main
# テスト対象のクラスでimportしてる外部クラス
from user import UserClass
def test_main_true(monkeypatch):
# インスタンスモックを作成
mock_user_ins = MagicMock(spec=UserClass)
# インスタンスモックのメソッドを設定
mock_user_ins.get_user.return_value = "saionji",11
# クラスモックに置き換える※グローバル変数を指定する
monkeypatch.setattr('main.user_instance', mock_user_ins)
# テスト実行
name, age = main.main()
assert name == "saionji"
assert age == 11

グローバル領域でインスタンスを生成した場合、インスタンスを生成は
UserClassのimport時に実施されているため、UserClassを置き換えても
それはインスタンス生成後なので意味がない。というか置き換えが失敗する

そのため生成されてグローバル変数に格納されたUserClassインスタンス自体を
直接、インスタンスモックに置き換える

外部クラス(クラスメソッド)をmockに置き換えるパターン

テスト対象のモジュール内で外部クラスをimportし、クラスメソッド
を使用している場合の置き換えパターン

ids.py

ids.py
class UserIdClass:
# ディクショナリ
users = [
{"user_id": 1, "name": "Tujimura", "age": 11},
{"user_id": 2, "name": "mori", "age": 20},
{"user_id": 3, "name": "shimada", "age": 50},
{"user_id": 4, "name": "kyogoku", "age": 70}
]
@classmethod
def get_id(cls, name):
user_id = next((user["user_id"] for user in cls.users if user["name"] == name), None)
return user_id
  • UserIdClassのクラスメソッドget_idでは引数のnameと一致するidを返却する
  • UserIdClassはクラスメソッドしかないため、インスタンス化なしで使用する

main.py

main.py
from ids import UserIdClass
def main():
id = UserIdClass.get_id(name="saikawa")
return id

-mainメソッドではUserIdClassのクラスメソッドを使用している

test_main.py

test_main.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
import main
def test_main_true(monkeypatch):
# UserIdClassのクラス関数を置き換え
monkeypatch.setattr('main.UserIdClass.get_id', MagicMock(return_value=99))
# テスト実行
id = main.main()
assert id == 99

クラス関数自体を指定してmockに置き変える

または

test_main.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
import main
def test_main_true(monkeypatch):
# クラスごと置き換える
mock_id_class = MagicMock()
mock_id_class.get_id.return_value = 99
monkeypatch.setattr('main.UserIdClass', mock_id_class)
# テスト実行
id = main.main()
assert id == 99

クラスごと置き換えることもできる

外部モジュールの関数をモックに置き換えるパターン

テスト対象のモジュール内で外部モジュールの関数をimportして使用している場合の置き換えパターン

utils.py

utils.py
def print_age(age:str):
return f'{age}歳です'
  • 引数を出力するだけの関数

main.py

main.py
from utils import print_age
def main(name:str):
print_age(name)

mainメソッドではutils.pyのprint_age関数をimportして使用している

test_main.py

test_main.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
import main
def test_main(monkeypatch):
# 関数をモック化
mock_func = MagicMock()
monkeypatch.setattr('main.print_age', mock_func)
# テスト実行
result = main.main("takuya")
# 関数の呼び出し確認
mock_func.assert_called_once_with("takuya")
  • 関数print_ageをmockに置き換え
  • 置き換えたmockが指定した引数「takuya」で呼ばれていることの確認

内部インスタンス関数を置き換えるパターン

自クラスの内部インスタンス関数を置き換えるパターン。
あるクラスのインスタンス関数で同じクラス内のインスタンス関数を使っている場合、
mockに置き換えることがで一つの関数に集中してテストできる。

user.py

user.py
class UserClass:
def __init__(self,name:str, age:int):
self.name = name
self.age = age
# インスタンスメソッド
def get_user(self):
return self.name, self.age
# インスタンスメソッド
def get_user_info_join(self):
user_str = self._user_info_join()
return user_str
# 内部インスタンスメソッド
def _user_info_join(self):
return f'{self.name}_{self.age}'
  • _user_info_joinは同じクラスのインスタンス内で使用される前提の内部インスタンスメソッド
  • get_user_info_joinは内部インスタンスメソッドである_user_info_joinを使用して処理している

test_user.py

test_user.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
from user import UserClass
# 内部インスタンスメソッドの置き換え
def test_get_user_info_join(monkeypatch):
# UserClassのインスタンスを生成
user_instance = UserClass(name="saikawa", age=23)
# UserClassのインスタンスの_user_info_joinメソッドを指定して置き換え
monkeypatch.setattr(user_instance, '_user_info_join', MagicMock(return_value="test"))
# テスト実行
result = user_instance.get_user_info_join()
assert result == "test"

引数が3つmonkeypatch.setattrメソッドを使って
UserClassクラスのインスタンス作成後にインスタンスのメソッドを指定して置き換えている。

または

test_user.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
from user import UserClass
# 内部インスタンスメソッドの置き換え
def test_get_user_info_join(monkeypatch):
# UserClassのインスタンスの生成前に置き換え
monkeypatch.setattr('user.UserClass._user_info_join', MagicMock(return_value="test"))
# UserClassのインスタンスを生成
user_instance = UserClass(name="saikawa", age=23)
# テスト実行
result = user_instance.get_user_info_join()
assert result == "test"

のようにUserClassクラスのインスタンス作成前に置き換えることもできる

内部インスタンス変数をmockにするパターン

自クラスの内部インスタンス変数をmockにするパターン

user.py

user.py
class UserClass:
def __init__(self,name:str, age:int):
self.name = name
self.age = age
# インスタンスメソッド
def get_user(self):
return self.name, self.age
# インスタンスメソッド
def get_user_info_join(self):
user_str = self._user_info_join()
return user_str
# 内部インスタンスメソッド
def _user_info_join(self):
return f'{self.name}_{self.age}'
  • 内部インスタンスメソッドはインスタンス変数を表示している

test_user.py

test_user.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
from user import UserClass
# インスタンス変数の置き換え
def test_user_info_join(monkeypatch):
user_instance = UserClass(name="saikawa", age=23)
# インスタンス変数の置きかえ
monkeypatch.setattr(user_instance, 'name', "takashi")
monkeypatch.setattr(user_instance, 'age', 45)
result = user_instance._user_info_join()
assert result == "takashi_45"
  • UserClassクラスのインスタンス生成後にインスタンス変数を置き換えする
  • インスタンス変数はインスタンス生成後に設定されるためインスタンス生成前の置き換えは不可。

置き換えの要点

作成したmockを対象のクラス、関数、変数と置き換える場合、
ちゃんと指定しないと上手くmockと置き換わらずテストが失敗する。

そのため下記の点に注意して置き換えを行う

置き換えた部分を含む処理はいつで動くのか?

クラスをインスタンス化して使うメソッドの置き換えで、置き換え処理より前に
動いてしまうと置き換えがうまくいかない 特にグローバル領域の処理はモジュールのimport時に動くため注意する。
※「外部クラスのインスタンスをmockに置き換えるパターン~その②~」のやつ

基本は本体ではなくimportしたものをmock化する

基本的には本体ではなく、テスト対象モジュールがインポートしている関数やクラスをモック化する。 理由としては下記になる

インポートした関数をモック化した方がいい理由

  • 実際にテスト対象がインポートしている関数やクラスをモック化することで、テストが依存関係に基づいて正しく動作する
  • モジュール間の依存関係が反映され、テスト対象がどの関数を使っているのかを正確にモック化できる
  • テストが信頼性高く、意図通りに動作する

本体をモック化しない方がいい理由

  • 本体をモック化しても、テスト対象が実際に参照している 関数とは異なる関数をモック化してしまうため、テストが意図通りに動作しないことがある
  • 依存関係が隠れ、テストが誤った挙動を確認することになり、信頼性が低くなる

まとめ

pytestのおけるmockの置き換えパターンをまとめてみた。
pytestでテストをしているとテスト失敗の原因は意図したとおりにmockに
置き換わっていないことが多くある。

また今回は置き換えはpytestのmonkeypatchを使っているが

  • @patch
  • @patch.object
  • with patch
  • with patch.object

を使うこともできる。
詳しくは下記参照

新着記事

目次
タグ別一覧
top