当サイトは、アフィリエイト広告を利用しています
pytestを実施する際にテスト対象モジュールとは関係のないモジュール等を
作成したモックに置き換える必要がある。
その際に下記の
を使って、何を作成したモックと置き換えるのか指定する
使い方が結構、ややこしくどれを使うべきか混乱したので
それぞれの使い方と使いどころを忘備録として残す
Pythonの標準ライブラリのunittestでテストする時に使う
「@patch」はモジュール内の関数や、特定のオブジェクトをモジュール単位でモック化する際に使う
要はクラスのインスタンスごとモックに変えたい場合等に使う
スコープ制御は
で設定できる
Pythonの標準ライブラリのunittestでテストする時に使う
「with patch」は「@patch」と使い方は同じだが スコープが違う。
「with patch」は指定したブロック内でモックを適用する
ブロックを抜けると、元のオブジェクトに戻る
関数スコープより細かいスコープで「@patch」使いたい場合は「with patch」を使う
Pythonの標準ライブラリのunittestでテストする時に使う
「@patch.object」はクラスやオブジェクトのメソッドや属性をモック化する際に使う。
要はクラスの一部のメソッドだけモックに変えたい場合等に使う
※@patchよりもモック化する範囲が小さいイメージ
スコープ制御は
で設定できる
Pythonの標準ライブラリのunittestでテストする時に使う
「with patch.object」は「@patch.object」と使い方は同じだが スコープが違う。
「with patch.object」は指定したブロック内でモックを適用する
ブロックを抜けると、元のオブジェクトに戻る
関数スコープより細かいスコープで「@patch.object」使いたい場合は「with patch.object」を使う
pytestを使ってテストをするときに使う
pytestは標準ライブラリではないので別途インストールがいる
「monkeypatch」は基本的には「@patch.object」と同じように
クラスやオブジェクトのメソッドや属性をモック化する際に使う。
また「@patch」のように特定のオブジェクトをモジュール単位でモック化する際に
使うこともできる。
それぞれの特徴を踏まえて、どのように使い分けるか整理してみる。
※整理はしてみたが、正直、ケースバイケースなのであくまで参考程度としてほしい。書き方が色々ありすぎるので...
unittestを使っている場合は
を使う
pytestの場合は
を使う
クラスのインスタンスを置き換える場合は
を使う
クラスの関数や変数を置き変えたい場合は
を使う
テストコードを
のパターン別に書いてみる。
※with patchとwith patch.objectはスコープが変わるだけので省く
プロジェクト構成は下記とする
.|-- app| |-- __init__.py| |-- class_module.py| `-- execute_module.py|`-- tests|-- __init__.py|-- test_execute_module_monkeypatch_class.py|-- test_execute_module_monkeypatch_class2.py|-- test_execute_module_monkeypatch_fanc.py|-- test_execute_module_monkeypatch_fanc2.py|-- test_execute_module_patch.py`-- test_execute_module_patch_object.py
これのapp/execute_module.pyのテストコード「test_execute_module.py」をパターン別に書く。
実行モジュール。
ExecuteClassでSampleClassのインスタンスを作成してインスタンスメソッドを実行する。
from app.class_module import SampleClassclass ExecuteClass:def __init__(self):self.sample_class = SampleClass(param1='1', param2='2', param3='3')def callParam1(self):return self.sample_class.getParam1()def callParam2(self):return self.sample_class.getParam2()if __name__ == "__main__":execute_instance = ExecuteClass()print(execute_instance.callParam1())print(execute_instance.callParam2())# 実行結果# SampleClassのparam1は「1」です# SampleClassのparam2は「2」です
ExecuteClassテストではExecuteClass内で呼んでいる
をmock化してテスト実施する
SampleClassはexecute_module/ExecuteClassでインスタンス化して実行する
class SampleClass:def __init__(self,param1=None, param2=None, param3=None):self.param1 = param1self.param2 = param2self.param3 = param3def getParam1(self):return f'SampleClassのparam1は「{self.param1}」です'def getParam2(self):return f'SampleClassのparam2は「{self.param2}」です'
execute_module.pyのテストではExecuteClassクラスの単体テストを行う
そのため、ExecuteClassクラスのメソッドから呼び出している
class_module.pyのSampleClassについてはモック化してテストする。
下記からパターン別まとめる
@patchを使ってSampleClassインスタンスをテストクラススコープで
mock化してテスト実行する
import unittestfrom unittest.mock import patch# テスト対象のクラスfrom app.execute_module import ExecuteClass# テスト対象のExecuteClassクラス内で呼ばれるSampleClassクラスをmockにする@patch('app.execute_module.SampleClass')class TestSampleClass(unittest.TestCase):def test_callParam1(self, mock_sample_class):# mockインスタンスを取得mock_instance = mock_sample_class.return_value# mockのメソッドを定義mock_instance.getParam1.return_value = "param1"# test実行execute_instance = ExecuteClass()result = execute_instance.callParam1()# callNameの戻り値が期待通りのものであるかをアサートassert result == "param1"def test_callParam2(self, mock_sample_class):# mockインスタンスを取得mock_instance = mock_sample_class.return_value# mockのメソッドを定義mock_instance.getParam2.return_value = "param2"# test実行execute_instance = ExecuteClass()result = execute_instance.callParam2()# callNameの戻り値が期待通りのものであるかをアサートassert result == "param2"
部分的に解説する。
※test_callParam2はtest_callParam1と同じなので省く。
# テスト対象のExecuteClassクラス内で呼ばれるSampleClassクラスをmockにする@patch('app.execute_module.SampleClass')class TestSampleClass(unittest.TestCase):...
execute_module.py内で呼ばれているSampleClassをmockの置き換え対象に指定している。
注意としては「execute_module.py内で呼ばれているSampleClass」なので
「app.execute_module.SampleClass」を指定すること。
※「app.class_module.SampleClass」ではない点に注意。
テストクラススコープではテストクラスの上で@patchデコレーターを宣言する。
...def test_callParam1(self, mock_sample_class):# mockインスタンスを取得mock_instance = mock_sample_class.return_value# mockのメソッドを定義mock_instance.getParam1.return_value = "param1"# test実行execute_instance = ExecuteClass()result = execute_instance.callParam1()# callNameの戻り値が期待通りのものであるかをアサートassert result == "param1"...
テストクラスの関数なので、引数として「self」がいる。
また第二引数である「mock_sample_class」は@patchデコレートによってモックにされた
「SampleClass」が入っている。
そのモックのgetParam1メソッドを定義してテストを実行している。
デバッグしてみればわかるが、ExecuteClassのcallParam1メソッド内で
呼ばれているsample_classのgetParam1の戻り値が"param1"を返すようになってる
※モックに置き換えているため
関数スコープにした場合は@patchデコレーターをテスト関数の上につければいい。
@patch.objectを使ってSampleClassのメソッドをテスト関数スコープで
mock化してテスト実行する
import unittestfrom unittest.mock import patch# テスト対象のクラスfrom app.execute_module import ExecuteClassfrom app.class_module import SampleClassclass TestSampleClass(unittest.TestCase):# テスト関数@patch.object(SampleClass, 'getParam1')def test_callParam1(self, mock_sample_class_getParam1):# getParam1の戻り値を指定mock_sample_class_getParam1.return_value = "param1"# test実行execute_instance = ExecuteClass()result = execute_instance.callParam1()# callNameの戻り値が期待通りのものであるかをアサートassert result == "param1"@patch.object(SampleClass, 'getParam2')def test_callParam2(self, mock_sample_class_getParam2):# getParam2の戻り値を指定mock_sample_class_getParam2.return_value = "param2"# test実行execute_instance = ExecuteClass()result = execute_instance.callParam2()# callNameの戻り値が期待通りのものであるかをアサートassert result == "param2"
@patch.objectでSampleClassのgetParam1,getParam2メソッドを
モック化する。関数スコープのためデコレーターは関数の上で指定している
第二引数である「mock_sample_class_getParam1,2」は@patch.objectデコレータに
よってモックにされた「SampleClassのgetParam1,2」が入っているので
呼び出された時の戻り値をreturn_valueで指定して実行している
pytestのmonkeypatchを使ってSampleClassクラスをモック化する
初期処理を定義できるpytest.fixtureと併用する
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスfrom app.execute_module import ExecuteClass@pytest.fixturedef mock_sample_class(monkeypatch):# インスタンスのモックを作成mock_sample_class_instance = MagicMock()# メソッドの戻り値を設定mock_sample_class_instance.getParam1.return_value = "param1"mock_sample_class_instance.getParam2.return_value = "param2"# クラスのモックを生成mock_sample_class = MagicMock()# クラスのモックが呼ばれた場合にインスタンスモックが返るようにするmock_sample_class.return_value=mock_sample_class_instance# SampleClassをクラスモックに置き換えるmonkeypatch.setattr('app.execute_module.SampleClass', mock_sample_class)class TestSampleClass:def test_callParam1(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam1を呼び出し、結果を取得result = execute_instance.callParam1()# 戻り値をアサートassert result == "param1"def test_callParam2(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam2を呼び出し、結果を取得result = execute_instance.callParam2()# 戻り値をアサートassert result == "param2"
@pytest.fixtureで定義した関数をテスト関数の引数として渡すことで
そのテスト関数の実行前に初期処理として実行させることができる
モックとしては下記の二つのモックを作成している
そして、「monkeypatch.setattr~」の部分で
SampleClassをSampleClassクラスモックに置き換えている
こうすることで「execute_module/ExecuteClass」内でSampleClassのインスタンスを
作成した際にSampleClassインスタンスモックが返ることになる
テスト関数では第二引数として@pytest.fixtureで定義した「mock_sample_class」があるので
テスト実行前にSampleClassはクラスモックに置き換わっている
別の方法としては上記ではSampleClassクラス自体をモックに置き換えたが
ExecuteClassの初期処理を置き換えることもでも実現できる。
import pytestfrom unittest.mock import MagicMock# テスト対象のクラスfrom app.execute_module import ExecuteClass@pytest.fixturedef mock_sample_class(monkeypatch):# インスタンスモックの生成mock_sample_class_instance = MagicMock()mock_sample_class_instance.getParam1.return_value = "param1"mock_sample_class_instance.getParam2.return_value = "param2"# init処理を定義(引数の数は元のinit処理と同じにする)def mock_init(self):self.sample_class = mock_sample_class_instance# ExecuteClassのinit処理を置き換えて,インスタンスモックを返すようにするmonkeypatch.setattr(ExecuteClass, '__init__', mock_init)class TestSampleClass:def test_callParam1(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam1を呼び出し、結果を取得result = execute_instance.callParam1()# 戻り値をアサートassert result == "param1"def test_callParam2(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam2を呼び出し、結果を取得result = execute_instance.callParam2()# 戻り値をアサートassert result == "param2"
こちらはExecuteClassのinit処理を定義した「mock_init」で置き換えている。
作成したインスタンスモックを返すinit処理に置き換えただけであり
SampleClass自体はモックになっていない。
init処理を置き換える際の注意点としては元のinit処理と
引数の数は合わせた関数を定義する必要がある
※あまり使うことはないかもですが、一応載せときます。
pytestのmonkeypatchを使ってSampleClassクラスの関数をモック化する
import pytest# テスト対象のクラスfrom app.execute_module import ExecuteClassfrom app.class_module import SampleClassclass TestSampleClass:def test_callParam1(self, monkeypatch):# 関数をモック化monkeypatch.setattr(SampleClass, "getParam1", lambda self: "param1")# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam1を呼び出し、結果を取得result = execute_instance.callParam1()# 戻り値をアサートassert result == "param1"def test_callParam2(self, monkeypatch):# 関数をモック化monkeypatch.setattr(SampleClass, "getParam2", lambda self: "param2")# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam2を呼び出し、結果を取得result = execute_instance.callParam2()# 戻り値をアサートassert result == "param2"
こちらはクラスではなく、関数をモック化している
使い方は「@patch.object」と同じ感じになる
fixtureを使うことで複数の関数をまとめてモック化しておくこともできる
import pytest# テスト対象のクラスfrom app.execute_module import ExecuteClass# モック化するクラスfrom app.class_module import SampleClass@pytest.fixturedef mock_sample_class(monkeypatch):# 関数定義# 元の関数getParam1と同じ数の引数を設定する必要があるdef mock_func(*args, **kwargs):return 'param1'# SampleClassのgetParam1をmock_funcに置き換えmonkeypatch.setattr(SampleClass, 'getParam1', mock_func)# パスを指定することもできる。またラムダも使える# こちらも元の関数getParam2と同じ数の引数を設定する必要があるmonkeypatch.setattr('app.class_module.SampleClass.getParam2', lambda self :'param2')class TestSampleClass:def test_callParam1(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam1を呼び出し、結果を取得result = execute_instance.callParam1()# 戻り値をアサートassert result == "param1"def test_callParam2(self, mock_sample_class):# ExecuteClassのインスタンスを作成execute_instance = ExecuteClass()# callParam2を呼び出し、結果を取得result = execute_instance.callParam2()# 戻り値をアサートassert result == "param2"
元と関数(getParam1)と置き換えるモック関数(mock_func)については
引数を合わせておく必要がある。
そのため、どんな形でも引数を受け取れる
としている。
※コンストラクタを置き換える時はselfが必要になる
やっていることは同じだが、場合によってはfixtureでまとめて
モック化しておいた方がいい場合もあると思う。
※書き方のパターンを残したいのは敢えて統一してない
pythonの単体テストで使用されるunittestとpytestでモックを使う際に
その置き換え対象を指定する
について基本を整理してみた。
個人的にはmonkeypatchが使いやすかった。
unittestとpytestを併用する場合もあり
使い分けが難しいのでとりあえず色々なパターンをサンプルとして
残しておく。