当サイトは、アフィリエイト広告を利用しています
Pythonソースコードをテストする際にpytestやunittestを使って
テストを実施するが、その時、使うことになるモックオブジェクトついてまとめる
当記事ではざっくり
について解説する
mock(モック)は、プログラムのテスト時に、実際のオブジェクトや処理を模倣するための
をのこと。
通常、テスト対象のコードは他のオブジェクトや外部システムに依存しているが
モックを使うことでそれらの依存を切り離してテストできる。
たとえば、
のような構成でテストを行う場合 controllerモジュールとserviceモジュールは下記のようにテストする
serviceモジュールをテストするmodelモジュールをモック化する
controllerモジュールをテストする際は、serviceモジュールをモック化
このように
モジュールごとに依存部分をモック化することで
それぞれのロジックを分離し、独立してテスト可能になる
上記のようにテスト対象以外の依存するモジュールをモックオブジェクトに
置き換えてテストする方法のメリットを具体的あげると下記になる。
データベース、API、ファイルシステムなどの外部リソースがテスト対象のコードに含まれている場合、
これらを実際に使用するとテストが不安定になる。
モックを使うことで、テスト対象以外の部分がどのような状態でもテストが一貫して実施できる。
外部リソースや重い処理を実行すると、テストが遅くなる場合がある。
モックを使って即座に結果を返すことで高速でテストできる
特定の例外が発生する条件やエラーを再現するのが難しい場合、モックで意図的にエラーを発生させることで
エラーが発生した際の処理が正しく行われるかを簡単にテストできる
テスト対象のコードが特定の条件で動作するかを確認する場合
モックを使用すると余計な部分を省略できるので、対象のコードに集中できる。
pythonのテストで使用するモックは大きく
の3種類がある。
モックのプロパティをまとめてみる
モックオブジェクトが返す値や動作をカスタマイズするためのもの。
return_value
メソッドや関数が呼び出された際に返す値を指定。
side_effect
呼び出し時の副作用(例外をスローしたり、動的に結果を変える)を指定。
下記からサンプル実装して動作を確認する
from unittest.mock import Mockmock_obj = Mock()mock_obj.some_method.return_value = "mocked result"print(mock_obj.some_method()) # 出力: "mocked result"
モックメソッドが呼び出された際に返す値を設定する。
また
from unittest.mock import Mockmock_obj = Mock(return_value="default return value")print(mock_obj()) # 出力: "default return value"
のようにモック自体が呼ばれた時に値を返すこともできる。
こっちも以外と使う。
モックの動作をさらにカスタマイズするためのプロパティ.
のような使い方ができる
from unittest.mock import Mockmock = Mock()mock.some_method.side_effect = ValueError("An error occurred") # 例外を設定try:mock.some_method() # 例外を発生except ValueError as e:print(e) # 出力: "An error occurred"
from unittest.mock import Mockmock = Mock()mock.some_method.side_effect = [1, 2, 3] # 戻り値をリストで設定print(mock.some_method()) # 出力: 1print(mock.some_method()) # 出力: 2print(mock.some_method()) # 出力: 3
from unittest.mock import Mock# カスタム関数def custom_side_effect(arg):return f"Processed {arg}"mock = Mock()mock.some_method.side_effect = custom_side_effectprint(mock.some_method("test")) # 出力: "Processed test"
テスト中にモックがどのように呼び出されたかを検証するためのもの。
呼び出し状況を検証
assert_called
モックが1回以上呼び出されたことを確認。
assert_called_with
モックが最後に指定された引数で呼び出されたことを確認。
assert_called_once
モックが1回だけ呼び出されたことを確認。
assert_called_once_with
モックが1回だけ、指定された引数で呼び出されたことを確認。
assert_not_called
モックが一度も呼び出されていないことを確認。
呼び出し履歴を詳細に検証
下記からサンプル実装して動作を確認する
from unittest.mock import Mockdef test_sample():# モック作成mock = Mock(return_value="1")# モック呼び出しmock()# 検証mock.assert_called() # mockが呼びされているためOK
モックが少なくとも1回呼び出されたかを確認する
from unittest.mock import Mockdef test_sample():# mockを作成mock = Mock()# mockの関数を呼び出しmock.some_method(1, key="value")# 検証mock.some_method.assert_called_with(1, key="value") # 正常終了(例外なし)
モックメソッドが最後に特定の引数で呼び出されたかどうかを検証する。
テスト対象のモジュールからモックに置き換えたメソッドが
ちゃんと指定した引数で呼ばれているかを確認することができる
from unittest.mock import Mockdef test_sample():# mockを作成mock = Mock()# mockの関数をよびだしmock()# mock()# 検証mock.assert_called_once() # 正常終了(例外なし)
モックが1回だけ呼び出されたかを確認する。
※コメントアウトを消して2回呼ぶと失敗する
from unittest.mock import Mockdef test_sample2():# mockを作成mock = Mock()# mockの関数を呼び出しmock.some_method(1, key="value")# mock.some_method(1, key="value")# 検証mock.some_method.assert_called_once_with(1, key="value") # 正常終了(例外なし)
モックが1回だけ呼び出され、その引数が指定通りかを確認する。
※コメントアウトを消して2回呼ぶと失敗する
from unittest.mock import Mockdef test_sample():# mockを作成mock = Mock()# mock呼び出し# mock()# 検証mock.assert_not_called() # 正常終了(例外なし)
モックが1度も呼び出されていないことを確認する
from unittest.mock import Mockdef test_sample():# mockを作成mock = Mock()# mock呼び出しmock()# 検証mock.assert_any_call() # 正常終了(例外なし)
モックが1回でも呼ばれたかを確認する
モックの状態や履歴を確認するためのもの。
called
モックが1回以上呼び出されたかどうか(True/False)。
call_count
モックが呼び出された回数。
mock_calls
モックの呼び出し履歴(リスト形式)※引数とメソッドを含む。
call_args
モックメソッドが最後に呼び出された際の引数を記録する。
call_args_list
モックの呼び出し履歴(リスト形式)※引数のみ。
下記からサンプル実装して動作を確認する
from unittest.mock import Mock# mockを作成mock = Mock()# mock呼び出しmock()# 検証print(mock.called) #True
モックが1回以上呼び出されたかどうか(True/False)。
from unittest.mock import Mock# mockを作成mock = Mock()# mock呼び出しmock()mock()mock()# 検証print(mock.call_count) # 3
モックが呼び出された回数。
from unittest.mock import Mock,call# mockを作成mock = Mock()# mock呼び出しmock(1)mock(2)mock.some_method(3)# 検証print(mock.mock_calls) # [call(1), call(2), call.some_method(3)]
モックの呼び出し履歴(リスト形式)。
引数だけでなくメソッドもわかる。
from unittest.mock import Mockmock = Mock()mock.some_method(1, key="value")print(mock.some_method.call_args) # 出力: call(1, key='value')
モックメソッドが最後に呼び出された際の引数を記録する。
from unittest.mock import Mockmock = Mock()mock.some_method(1)mock.some_method(2, key="value")print(mock.some_method.call_args_list)# 出力: [call(1), call(2, key='value')]
call_argsが最後だけだったのに対して、こちらはすべての呼び出し履歴(引数のみ)を
確認できる
モックの構造や振る舞いを変更するためのもの。
configure_mock
モックの属性や振る舞いを動的に設定。
reset_mock
モックの呼び出し履歴や設定をリセット。
mock_add_spec
モックを特定のクラスや関数の仕様に従わせる(APIを制約する)。
from unittest.mock import Mock,callmock = Mock()mock.configure_mock(attr_a="value_a", attr_b="value_b")print(mock.attr_a) # "value_a"print(mock.attr_b) # "value_b"
モックオブジェクトの属性を動的に設定できる
from unittest.mock import Mock,call# モック作成mock = Mock()# モック呼び出しmock()mock()mock()# 呼び出し回数print(mock.call_count) # 3# リセットmock.reset_mock()# 呼び出し回数print(mock.call_count) # 0
モックの呼び出し履歴や属性をリセットできる
from unittest.mock import Mockclass MyClass:def method_a(self):return "a"def method_b(self):return "b"mock = Mock()# クラスを模倣させるmock.mock_add_spec(MyClass)mock.method_a() # 正常に動作mock.method_c() # AttributeError: 'Mock' object has no attribute 'method_c'
モックを特定のクラスや関数の仕様に従わせることができる
「mock = Mock(spec=MyClass)」との違いは
になる。
pythonのpytest等でモックを使う場合、基本的には
のような流れで作成して使用する。
次からモックの作成から使い方をまとめていく。
一番シンプルなMockを例にして説明する
空のMockオブジェクトを作成する。
from unittest.mock import Mock# 空のMockオブジェクトを作成mock_obj = Mock()print(mock_obj)# <Mock id='1515583465168'>
ただ空のMockを作っても意味がないので必要に応じて
属性やメソッドを追加する必要がある。
from unittest.mock import Mockclass MyClass:def my_method(self):pass# MyClassに基づいたMockオブジェクトmock_obj = Mock(spec=MyClass)mock_obj.my_method.return_value = "mocked result"print(mock_obj.my_method()) # 出力: "mocked result"# MyClassに存在しないメソッドを呼び出すとエラー# mock_obj.non_existent_method() # AttributeErrorが発生
spec引数を指定することで、特定のクラスやオブジェクトを基にして、存在するメソッドや属性のみを持つ
Mockオブジェクトを作成できる。
この方法で作成したMockは、指定したクラスやオブジェクトに存在しないメソッドや
属性を呼び出そうとするとエラーが発生になる
モックを作れたら、属性やメソッドなどを追加して
置き換えるクラスやメソッドの動作を再現できるように調整する
from unittest.mock import Mock# 特定の属性を持つMockオブジェクトmock_obj = Mock()mock_obj.some_attr = "mocked attribute"print(mock_obj.some_attr) # 出力: "mocked attribute"
Mockに属性を追加する
from unittest.mock import Mock# 特定のメソッドを持つMockオブジェクトmock_obj = Mock()mock_obj.some_method.return_value = "mocked result"print(mock_obj.some_method()) # 出力: "mocked result"
Mockにメソッドを追加する。
from unittest.mock import Mock# Mockオブジェクト自体の戻値を設定mock_obj = Mock()mock_obj.return_value = "mocked result"print(mock_obj.()) # 出力: "mocked result"# Mockオブジェクト作成時に指定する場合mock_obj = Mock(return_value = "mocked result")print(mock_obj()) # 出力: "mocked result"
作成したモックを任意のメソッドやクラスと置き換える。
実際に簡単なテストを作成して実行してみる
またモックの置き換えには
などを使う。
今回はmonkeypatchを使っていく
その他について詳しくは下記記事参照
.|-- app| |-- main.py| `-- sample_class_A.py`-- tests`-- test_main.py
上記のような構成で「main.py」のテストを「test_main.py」で行う
テスト対象コード。
sample_class_A.pyのSampleClassAに依存している
from app.sample_class_A import SampleClassAdef main():result = SampleClassA.getOne()print(result)if __name__ == "__main__":main()
SampleClassAのメソッドを読んで出力するモジュール
class SampleClassA:@staticmethoddef getOne():return 1
一つのスタティック関数を持つクラス。
「SampleClassA.getOne()」をmockにしてテストをする。
こうすることでテスト対象の関数 main() の動作のみを独立して確認できる
from unittest.mock import Mock, patchfrom app.main import maindef test_main(monkeypatch):# SampleClassA.getOne をモック化mock_getOne = Mock(return_value="mocked result")# monkeypatchで置き換えmonkeypatch.setattr('app.main.SampleClassA.getOne', mock_getOne)# main 関数を実行main()# モックが正しく呼び出されたことを確認mock_getOne.assert_called_once_with()
値を返すモックを作成してmonkeypatchで置き換えている。
置き換える対象は
である点に注意する。
理由は
のため。
import元の方を指定するとうまくモック化できなかったりするので注意。
上記ではgetOneメソッドを置き換えたがクラスごと置き換えることもできる
from unittest.mock import Mock, patchfrom app.sample_class_A import SampleClassAfrom app.main import mainfrom unittest.mock import Mock, patchfrom app.main import maindef test_main(monkeypatch):# SampleClassAをモック化mock_class_a = Mock(spec=SampleClassA)# メソッドの戻り値を指定mock_class_a.getOne.return_value = "mocked result"# 置き換えmonkeypatch.setattr('app.main.SampleClassA', mock_class_a)# main 関数を実行main()# モックが正しく呼び出されたことを確認mock_class_a.getOne.assert_called_once_with()
何をどのようにモック化する化はテストによって適宜決める。
プロジェクトのルートでpytestを実行することでテストを実行できる
$ pytest================================================= test session starts =================================================platform win32 -- Python 3.11.1, pytest-8.2.2, pluggy-1.5.0rootdir: C:\develop\01_TechSamples\Python\Pytest\pytest_mockplugins: cov-5.0.0collected 1 itemtests\test_main.py . [100%]================================================== 1 passed in 0.08s ==================================================
pytestはVSCode上での実行やカバレッジの確認などもできる。
詳しい使い方は下記記事でまとめている
Pythonのテストを使う時に必須となるモックオブジェクトについて
その作り方と使い方をまとめてみた。
当記事ではモックについてメインでまとめたため
モックの置き換え方法については極々、簡単なものなっているが
置き換え対象(クラスなのかメソッドなおか、インスタンスなのか?等)によって
置き換え方法が複雑になるので、その辺を今後、まとめていきたい。