当サイトは、アフィリエイト広告を利用しています
Pythonでアプリケーション等の動作確認をpytestを使って行う場合
テスト対象のモジュール以外をmockに置き換えることで効率的に
テスト対象モジュールの動作を確認できる。
当記事では、そのテスト対象モジュール以外の部分を
ついてまとめておく。
Mockオブジェクト自体の使い方やmockを使うことのメリットについては
下記記事でまとめているので当記事と合わせて参照してほしい
たとえば、テスト対象モジュール
があり、mainモジュールからclassモジュールをよんで処理をしている場合
mainモジュール内でclassモジュールを使っている部分はmockに置き換えて
テストを行う。
こうすることでテスト対象モジュールであるmainモジュールの
テストに集中することができる。
pythonのpytestでモックを使う場合、基本的には
のような流れで作成して使用する。
今回はこの手順の「モックオブジェクトを任意のメソッドやクラス、インスタンスと置き換える」の部分に
フォーカスしてまとめる。
mockへの置き換えはpytestのmonkeypatchを使って行う。
monkeypatchは
がある。
# モジュールやクラスを文字列で指定する場合monkeypatch.setattr("module_path.attribute_name", mock_value)
# クラスやモジュールを直接参照できる場合monkeypatch.setattr(target_object, "attribute_name", mock_value)
サンプルでいくつかモジュールを作り、
よくあるmockの置き換えパターンをまとめる。
下記の4つのモジュールを作る
main.pyは
のクラスや関数をimportして処理をしている
上記の4つもモジュールを編集しながらよく出る置き換えパターンのサンプルをまとめる
テスト対象のモジュール内で外部クラスをimportし、インスタンス化して
使用している場合の置き換えパターン
class UserClass:def __init__(self,name:str, age:int):self.name = nameself.age = age# インスタンスメソッドdef get_user(self):return self.name, self.age
main.pyからget_userを使う
from user import UserClassdef main():# UserClassインスタンス作成user_instance = UserClass(name="saikawa", age=23)# UserClassのインスタンスメソッドを呼び出しname, age = user_instance.get_user()return name, ageif __name__ == "__main__":print(main())
mainメソッドではハイライト部分は UserClassのインスタンスメソッドを使っている。
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスimport main# テスト対象のクラスでimportしてる外部クラスfrom user import UserClassdef 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)
# インスタンスモックのメソッドを設定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()
main関数内でUserClassのインスタンス生成処理がモックの置き換わった状態で
テスト実行される
上記のパターンではmainメソッド内でUserClassのインスタンスを生成していたが
グローバル領域で生成していた場合はそのグローバル変数を置き換える必要がある。
from user import UserClass# グローバル領域でインスタンス生成user_instance = UserClass(name="saikawa", age=23)def main():name, age = user_instance.get_user()return name, age
import main# テスト対象のクラスでimportしてる外部クラスfrom user import UserClassdef 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インスタンス自体を
直接、インスタンスモックに置き換える
テスト対象のモジュール内で外部クラスをimportし、クラスメソッド
を使用している場合の置き換えパターン
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}]@classmethoddef get_id(cls, name):user_id = next((user["user_id"] for user in cls.users if user["name"] == name), None)return user_id
from ids import UserIdClassdef main():id = UserIdClass.get_id(name="saikawa")return id
-mainメソッドではUserIdClassのクラスメソッドを使用している
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスimport maindef test_main_true(monkeypatch):# UserIdClassのクラス関数を置き換えmonkeypatch.setattr('main.UserIdClass.get_id', MagicMock(return_value=99))# テスト実行id = main.main()assert id == 99
クラス関数自体を指定してmockに置き変える
または
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスimport maindef test_main_true(monkeypatch):# クラスごと置き換えるmock_id_class = MagicMock()mock_id_class.get_id.return_value = 99monkeypatch.setattr('main.UserIdClass', mock_id_class)# テスト実行id = main.main()assert id == 99
クラスごと置き換えることもできる
テスト対象のモジュール内で外部モジュールの関数をimportして使用している場合の置き換えパターン
def print_age(age:str):return f'{age}歳です'
from utils import print_agedef main(name:str):print_age(name)
mainメソッドではutils.pyのprint_age関数をimportして使用している
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスimport maindef test_main(monkeypatch):# 関数をモック化mock_func = MagicMock()monkeypatch.setattr('main.print_age', mock_func)# テスト実行result = main.main("takuya")# 関数の呼び出し確認mock_func.assert_called_once_with("takuya")
自クラスの内部インスタンス関数を置き換えるパターン。
あるクラスのインスタンス関数で同じクラス内のインスタンス関数を使っている場合、
mockに置き換えることがで一つの関数に集中してテストできる。
class UserClass:def __init__(self,name:str, age:int):self.name = nameself.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}'
import pytestfrom 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クラスのインスタンス作成後にインスタンスのメソッドを指定して置き換えている。
または
import pytestfrom 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にするパターン
class UserClass:def __init__(self,name:str, age:int):self.name = nameself.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}'
import pytestfrom 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"
作成したmockを対象のクラス、関数、変数と置き換える場合、
ちゃんと指定しないと上手くmockと置き換わらずテストが失敗する。
そのため下記の点に注意して置き換えを行う
クラスをインスタンス化して使うメソッドの置き換えで、置き換え処理より前に
動いてしまうと置き換えがうまくいかない
特にグローバル領域の処理はモジュールのimport時に動くため注意する。
※「外部クラスのインスタンスをmockに置き換えるパターン~その②~」のやつ
基本的には本体ではなく、テスト対象モジュールがインポートしている関数やクラスをモック化する。 理由としては下記になる
pytestのおけるmockの置き換えパターンをまとめてみた。
pytestでテストをしているとテスト失敗の原因は意図したとおりにmockに
置き換わっていないことが多くある。
また今回は置き換えはpytestのmonkeypatchを使っているが
を使うこともできる。
詳しくは下記参照