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

【Python】unittes,pytestのpatch,monkeypatchの使い方

作成日:2024月06月23日
更新日:2024年06月27日

pytestを実施する際にテスト対象モジュールとは関係のないモジュール等を
作成したモックに置き換える必要がある。

その際に下記の

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

を使って、何を作成したモックと置き換えるのか指定する

使い方が結構、ややこしくどれを使うべきか混乱したので
それぞれの使い方と使いどころを忘備録として残す

@patch

Pythonの標準ライブラリのunittestでテストする時に使う

「@patch」はモジュール内の関数や、特定のオブジェクトをモジュール単位でモック化する際に使う
要はクラスのインスタンスごとモックに変えたい場合等に使う

スコープ制御は

  • クラススコープ
  • 関数スコープ

で設定できる

with patch

Pythonの標準ライブラリのunittestでテストする時に使う

「with patch」は「@patch」と使い方は同じだが スコープが違う。 「with patch」は指定したブロック内でモックを適用する
ブロックを抜けると、元のオブジェクトに戻る

関数スコープより細かいスコープで「@patch」使いたい場合は「with patch」を使う

@patch.object

Pythonの標準ライブラリのunittestでテストする時に使う

「@patch.object」はクラスやオブジェクトのメソッドや属性をモック化する際に使う。
要はクラスの一部のメソッドだけモックに変えたい場合等に使う
※@patchよりもモック化する範囲が小さいイメージ

スコープ制御は

  • 関数スコープ
  • クラススコープ

で設定できる

with patch.object

Pythonの標準ライブラリのunittestでテストする時に使う

「with patch.object」は「@patch.object」と使い方は同じだが スコープが違う。 「with patch.object」は指定したブロック内でモックを適用する
ブロックを抜けると、元のオブジェクトに戻る

関数スコープより細かいスコープで「@patch.object」使いたい場合は「with patch.object」を使う

monkeypatch

pytestを使ってテストをするときに使う
pytestは標準ライブラリではないので別途インストールがいる

「monkeypatch」は基本的には「@patch.object」と同じように
クラスやオブジェクトのメソッドや属性をモック化する際に使う。

また「@patch」のように特定のオブジェクトをモジュール単位でモック化する際に
使うこともできる。

使い分け

それぞれの特徴を踏まえて、どのように使い分けるか整理してみる。
※整理はしてみたが、正直、ケースバイケースなのであくまで参考程度としてほしい。書き方が色々ありすぎるので...

テストフレームワークは何を使っているか?

unittestを使っている場合は

  • @patch
  • @patch.object

を使う

pytestの場合は

  • monkeypatch

を使う

モックで置き換えたいものは何か?

クラスのインスタンスを置き換える場合は

  • @patch
  • monkeypatch

を使う

クラスの関数や変数を置き変えたい場合は

  • @patch.object
  • monkeypatch

を使う

テストコードを書いてみる

テストコードを

  • @patch
  • @patch.object
  • monkeypatch

のパターン別に書いてみる。
※with patchとwith patch.objectはスコープが変わるだけので省く

テスト対象のPJ構成

プロジェクト構成は下記とする

bash
.
|-- 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」をパターン別に書く。

execute_module.py

実行モジュール。
ExecuteClassでSampleClassのインスタンスを作成してインスタンスメソッドを実行する。

execute_module.py
from app.class_module import SampleClass
class 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内で呼んでいる

  • SampleClass

をmock化してテスト実施する

class_module.py

SampleClassはexecute_module/ExecuteClassでインスタンス化して実行する

class_module.py
class SampleClass:
def __init__(self,param1=None, param2=None, param3=None):
self.param1 = param1
self.param2 = param2
self.param3 = param3
def getParam1(self):
return f'SampleClassのparam1は「{self.param1}」です'
def getParam2(self):
return f'SampleClassのparam2は「{self.param2}」です'

execute_module.pyのテストについて

execute_module.pyのテストではExecuteClassクラスの単体テストを行う

そのため、ExecuteClassクラスのメソッドから呼び出している
class_module.pyのSampleClassについてはモック化してテストする。

下記からパターン別まとめる

@patchを使う場合_SampleClassをモック化

@patchを使ってSampleClassインスタンスをテストクラススコープで
mock化してテスト実行する

test_execute_module_patch.py
import unittest
from 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と同じなので省く。

@patch('app.execute_module.SampleClass') について

py
# テスト対象の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について

test_callParam1
...
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の関数モック化

@patch.objectを使ってSampleClassのメソッドをテスト関数スコープで
mock化してテスト実行する

test_execute_module_patch_object.py
import unittest
from unittest.mock import patch
# テスト対象のクラス
from app.execute_module import ExecuteClass
from app.class_module import SampleClass
class 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で指定して実行している

monkeypatchを使う場合_SampleClassクラスをモック化

pytestのmonkeypatchを使ってSampleClassクラスをモック化する
初期処理を定義できるpytest.fixtureと併用する

test_execute_module_monkeypatch_class.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
from app.execute_module import ExecuteClass
@pytest.fixture
def 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で定義した関数をテスト関数の引数として渡すことで
そのテスト関数の実行前に初期処理として実行させることができる

モックとしては下記の二つのモックを作成している

  • SampleClassインスタンスモック
  • SampleClassクラスモック

そして、「monkeypatch.setattr~」の部分で
SampleClassをSampleClassクラスモックに置き換えている

こうすることで「execute_module/ExecuteClass」内でSampleClassのインスタンスを
作成した際にSampleClassインスタンスモックが返ることになる

テスト関数では第二引数として@pytest.fixtureで定義した「mock_sample_class」があるので
テスト実行前にSampleClassはクラスモックに置き換わっている

monkeypatchを使う場合_SampleClassクラスをモック化~その2~

別の方法としては上記ではSampleClassクラス自体をモックに置き換えたが
ExecuteClassの初期処理を置き換えることもでも実現できる。

test_execute_module_monkeypatch_class2.py
import pytest
from unittest.mock import MagicMock
# テスト対象のクラス
from app.execute_module import ExecuteClass
@pytest.fixture
def 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処理と
引数の数は合わせた関数を定義する必要がある
※あまり使うことはないかもですが、一応載せときます。

monkeypatchを使う場合_SampleClassの関数モック化

pytestのmonkeypatchを使ってSampleClassクラスの関数をモック化する

test_execute_module_monkeypatch_fanc.py
import pytest
# テスト対象のクラス
from app.execute_module import ExecuteClass
from app.class_module import SampleClass
class 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」と同じ感じになる

monkeypatchを使う場合_SampleClassの関数モック化~その2~

fixtureを使うことで複数の関数をまとめてモック化しておくこともできる

test_execute_module_monkeypatch_fanc2.py
import pytest
# テスト対象のクラス
from app.execute_module import ExecuteClass
# モック化するクラス
from app.class_module import SampleClass
@pytest.fixture
def 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)については
引数を合わせておく必要がある。

そのため、どんな形でも引数を受け取れる

  • 「*args, **kwargs」

としている。
※コンストラクタを置き換える時はselfが必要になる

やっていることは同じだが、場合によってはfixtureでまとめて
モック化しておいた方がいい場合もあると思う。
※書き方のパターンを残したいのは敢えて統一してない

まとめ

pythonの単体テストで使用されるunittestとpytestでモックを使う際に
その置き換え対象を指定する

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

について基本を整理してみた。
個人的にはmonkeypatchが使いやすかった。

unittestとpytestを併用する場合もあり
使い分けが難しいのでとりあえず色々なパターンをサンプルとして
残しておく。

参考

関連記事

新着記事

top