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

【Python】pytestのMockの作成方法と使い方

作成日:2024月11月27日
更新日:2024年11月27日

Pythonソースコードをテストする際にpytestやunittestを使って
テストを実施するが、その時、使うことになるモックオブジェクトついてまとめる

当記事ではざっくり

  • モックオブジェクトとは?
  • なぜモックオブジェクトを使うのか?
  • モックオブジェクトの作成方法
  • モックオブジェクトの使い方

について解説する

モックオブジェクトとは?

mock(モック)は、プログラムのテスト時に、実際のオブジェクトや処理を模倣するための

  • ダミーオブジェクト

をのこと。

通常、テスト対象のコードは他のオブジェクトや外部システムに依存しているが
モックを使うことでそれらの依存を切り離してテストできる。

なぜモックオブジェクトを使うのか?

たとえば、

  • controllerモジュール:エンドポイントのリクエストを受け取り、serviceモジュールを呼び出す
  • serviceモジュール:ビジネスロジックを担当し、modelモジュールを通じてデータの読み書きを行う
  • modelモジュール:データベースアクセスなど、実際のデータ操作を行う

のような構成でテストを行う場合 controllerモジュールとserviceモジュールは下記のようにテストする

  • serviceモジュールをテストするmodelモジュールをモック化する

    • データベースの状態に依存しないテストができる
  • controllerモジュールをテストする際は、serviceモジュールをモック化

    • ビジネスロジックに依存しない、リクエストとレスポンスのテストができる

このように
モジュールごとに依存部分をモック化することで
それぞれのロジックを分離し、独立してテスト可能になる

モックを使ってテストするメリット

上記のようにテスト対象以外の依存するモジュールをモックオブジェクトに
置き換えてテストする方法のメリットを具体的あげると下記になる。

1. 外部リソースへの依存をなくせる

データベース、API、ファイルシステムなどの外部リソースがテスト対象のコードに含まれている場合、
これらを実際に使用するとテストが不安定になる。 モックを使うことで、テスト対象以外の部分がどのような状態でもテストが一貫して実施できる。

2. テストの速度を向上

外部リソースや重い処理を実行すると、テストが遅くなる場合がある。
モックを使って即座に結果を返すことで高速でテストできる

3. エラーや例外処理の再現性

特定の例外が発生する条件やエラーを再現するのが難しい場合、モックで意図的にエラーを発生させることで
エラーが発生した際の処理が正しく行われるかを簡単にテストできる

4. テスト対象のコードに集中できる

テスト対象のコードが特定の条件で動作するかを確認する場合
モックを使用すると余計な部分を省略できるので、対象のコードに集中できる。

モックの種類

pythonのテストで使用するモックは大きく

  • Mock
  • MagicMock
  • AsyncMock

の3種類がある。

違いとしては下記のようなイメージ 2024-11-12-23-00-37

モックで使うプロパティ

モックのプロパティをまとめてみる

1. モックの振る舞いをカスタマイズするプロパティ

モックオブジェクトが返す値や動作をカスタマイズするためのもの。

  • return_value
    メソッドや関数が呼び出された際に返す値を指定。

  • side_effect
    呼び出し時の副作用(例外をスローしたり、動的に結果を変える)を指定。

下記からサンプル実装して動作を確認する

return_value

return_value_1
from unittest.mock import Mock
mock_obj = Mock()
mock_obj.some_method.return_value = "mocked result"
print(mock_obj.some_method()) # 出力: "mocked result"

モックメソッドが呼び出された際に返す値を設定する。

また

return_value_2
from unittest.mock import Mock
mock_obj = Mock(return_value="default return value")
print(mock_obj()) # 出力: "default return value"

のようにモック自体が呼ばれた時に値を返すこともできる。
こっちも以外と使う。

side_effect

モックの動作をさらにカスタマイズするためのプロパティ.

のような使い方ができる

  • 例外を発生させる
例外発生
from unittest.mock import Mock
mock = 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 Mock
mock = Mock()
mock.some_method.side_effect = [1, 2, 3] # 戻り値をリストで設定
print(mock.some_method()) # 出力: 1
print(mock.some_method()) # 出力: 2
print(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_effect
print(mock.some_method("test")) # 出力: "Processed test"

2. モックの呼び出し履歴を検証するメソッド

テスト中にモックがどのように呼び出されたかを検証するためのもの。

  • 呼び出し状況を検証

    • assert_called
      モックが1回以上呼び出されたことを確認。

    • assert_called_with
      モックが最後に指定された引数で呼び出されたことを確認。

    • assert_called_once
      モックが1回だけ呼び出されたことを確認。

    • assert_called_once_with
      モックが1回だけ、指定された引数で呼び出されたことを確認。

    • assert_not_called
      モックが一度も呼び出されていないことを確認。

  • 呼び出し履歴を詳細に検証

    • assert_any_call
      指定した引数で1回でも呼び出されたことを確認。

下記からサンプル実装して動作を確認する

assert_called

assert_called
from unittest.mock import Mock
def test_sample():
# モック作成
mock = Mock(return_value="1")
# モック呼び出し
mock()
# 検証
mock.assert_called() # mockが呼びされているためOK

モックが少なくとも1回呼び出されたかを確認する

assert_called_with

assert_called_with
from unittest.mock import Mock
def test_sample():
# mockを作成
mock = Mock()
# mockの関数を呼び出し
mock.some_method(1, key="value")
# 検証
mock.some_method.assert_called_with(1, key="value") # 正常終了(例外なし)

モックメソッドが最後に特定の引数で呼び出されたかどうかを検証する。
テスト対象のモジュールからモックに置き換えたメソッドが
ちゃんと指定した引数で呼ばれているかを確認することができる

assert_called_once

assert_called_once
from unittest.mock import Mock
def test_sample():
# mockを作成
mock = Mock()
# mockの関数をよびだし
mock()
# mock()
# 検証
mock.assert_called_once() # 正常終了(例外なし)

モックが1回だけ呼び出されたかを確認する。
※コメントアウトを消して2回呼ぶと失敗する

assert_called_once_with

assert_called_once_with
from unittest.mock import Mock
def 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回呼ぶと失敗する

assert_not_called

assert_not_called
from unittest.mock import Mock
def test_sample():
# mockを作成
mock = Mock()
# mock呼び出し
# mock()
# 検証
mock.assert_not_called() # 正常終了(例外なし)

モックが1度も呼び出されていないことを確認する

assert_any_call

assert_any_call
from unittest.mock import Mock
def test_sample():
# mockを作成
mock = Mock()
# mock呼び出し
mock()
# 検証
mock.assert_any_call() # 正常終了(例外なし)

モックが1回でも呼ばれたかを確認する

3. モックの呼び出し履歴にアクセスするプロパティ

モックの状態や履歴を確認するためのもの。

  • called
    モックが1回以上呼び出されたかどうか(True/False)。

  • call_count
    モックが呼び出された回数。

  • mock_calls
    モックの呼び出し履歴(リスト形式)※引数とメソッドを含む。

  • call_args
    モックメソッドが最後に呼び出された際の引数を記録する。

  • call_args_list
    モックの呼び出し履歴(リスト形式)※引数のみ。

下記からサンプル実装して動作を確認する

called

called
from unittest.mock import Mock
# mockを作成
mock = Mock()
# mock呼び出し
mock()
# 検証
print(mock.called) #True

モックが1回以上呼び出されたかどうか(True/False)。

call_count

call_count
from unittest.mock import Mock
# mockを作成
mock = Mock()
# mock呼び出し
mock()
mock()
mock()
# 検証
print(mock.call_count) # 3

モックが呼び出された回数。

mock_calls

mock_calls
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)]

モックの呼び出し履歴(リスト形式)。
引数だけでなくメソッドもわかる。

call_args

call_args
from unittest.mock import Mock
mock = Mock()
mock.some_method(1, key="value")
print(mock.some_method.call_args) # 出力: call(1, key='value')

モックメソッドが最後に呼び出された際の引数を記録する。

call_args_list

call_args_list
from unittest.mock import Mock
mock = 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が最後だけだったのに対して、こちらはすべての呼び出し履歴(引数のみ)を
確認できる

4. モックオブジェクトの構造を変更・管理するメソッド

モックの構造や振る舞いを変更するためのもの。

  • configure_mock
    モックの属性や振る舞いを動的に設定。

  • reset_mock
    モックの呼び出し履歴や設定をリセット。

  • mock_add_spec
    モックを特定のクラスや関数の仕様に従わせる(APIを制約する)。

configure_mock

configure_mock
from unittest.mock import Mock,call
mock = Mock()
mock.configure_mock(attr_a="value_a", attr_b="value_b")
print(mock.attr_a) # "value_a"
print(mock.attr_b) # "value_b"

モックオブジェクトの属性を動的に設定できる

reset_mock

reset_mock
from unittest.mock import Mock,call
# モック作成
mock = Mock()
# モック呼び出し
mock()
mock()
mock()
# 呼び出し回数
print(mock.call_count) # 3
# リセット
mock.reset_mock()
# 呼び出し回数
print(mock.call_count) # 0

モックの呼び出し履歴や属性をリセットできる

mock_add_spec

mock_add_spec
from unittest.mock import Mock
class 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)」との違いは

  • specはモック作成時に指定する引数で、モックを作る時にそのクラスやインターフェースを模倣させる
  • mock_add_spec は、すでに作成されたモックに対して、後からそのクラスやインターフェースを模倣させる

になる。

モックの作成から使い方の流れ

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

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

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

次からモックの作成から使い方をまとめていく。

モックオブジェクトを作る

一番シンプルなMockを例にして説明する

空Mockオブジェクトを作成

空のMockオブジェクトを作成する。

空Mockオブジェクトを作成
from unittest.mock import Mock
# 空のMockオブジェクトを作成
mock_obj = Mock()
print(mock_obj)
# <Mock id='1515583465168'>

ただ空のMockを作っても意味がないので必要に応じて
属性やメソッドを追加する必要がある。

クラスやオブジェクトに基づいたMockを作成

spec使用
from unittest.mock import Mock
class 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"

任意のメソッドやクラス、インスタンスと置き換える

作成したモックを任意のメソッドやクラスと置き換える。
実際に簡単なテストを作成して実行してみる

またモックの置き換えには

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

などを使う。
今回はmonkeypatchを使っていく

その他について詳しくは下記記事参照

構成

bash
.
|-- app
| |-- main.py
| `-- sample_class_A.py
`-- tests
`-- test_main.py

上記のような構成で「main.py」のテストを「test_main.py」で行う

main.py

テスト対象コード。
sample_class_A.pyのSampleClassAに依存している

main.py
from app.sample_class_A import SampleClassA
def main():
result = SampleClassA.getOne()
print(result)
if __name__ == "__main__":
main()

SampleClassAのメソッドを読んで出力するモジュール

sample_class_A.py

sample_class_A.py
class SampleClassA:
@staticmethod
def getOne():
return 1

一つのスタティック関数を持つクラス。

test_main.py

「SampleClassA.getOne()」をmockにしてテストをする。

こうすることでテスト対象の関数 main() の動作のみを独立して確認できる

test_main.py
from unittest.mock import Mock, patch
from app.main import main
def 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で置き換えている。
置き換える対象は

  • mainモジュールにimportされている「SampleClassA」

である点に注意する。
理由は

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

のため。

import元の方を指定するとうまくモック化できなかったりするので注意。

上記ではgetOneメソッドを置き換えたがクラスごと置き換えることもできる

クラスごと置き換え
from unittest.mock import Mock, patch
from app.sample_class_A import SampleClassA
from app.main import main
from unittest.mock import Mock, patch
from app.main import main
def 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
$ pytest
================================================= test session starts =================================================
platform win32 -- Python 3.11.1, pytest-8.2.2, pluggy-1.5.0
rootdir: C:\develop\01_TechSamples\Python\Pytest\pytest_mock
plugins: cov-5.0.0
collected 1 item
tests\test_main.py . [100%]
================================================== 1 passed in 0.08s ==================================================

pytestはVSCode上での実行やカバレッジの確認などもできる。
詳しい使い方は下記記事でまとめている

dockerコンテナ上で行う方法だが、別にローカルでも同じことをすれば実行できる

まとめ

Pythonのテストを使う時に必須となるモックオブジェクトについて
その作り方と使い方をまとめてみた。

当記事ではモックについてメインでまとめたため
モックの置き換え方法については極々、簡単なものなっているが
置き換え対象(クラスなのかメソッドなおか、インスタンスなのか?等)によって
置き換え方法が複雑になるので、その辺を今後、まとめていきたい。

関連記事

新着記事

目次
タグ別一覧
top