概要
pytest
pytestは、Python 用のシンプルで柔軟なテストフレームワークです。環境構築が容易で書きやすく、Pythonのテストフレームワークでファーストチョイスになるツールです。本記事ではpytestで利用できるモックについて説明をします。もしpytestについておさらいしたい方や基本を確認したい方は下記の記事をご参考ください。
モックとパッチ
一般にモックはオブジェクトそのもののダミーを作成する行為やそのオブジェクトを指し、パッチはそのモックやダミーオブジェクトを適用して元のオブジェクトを一時的に置き換える行為を指します。私の認識ではモックはパッチを含む大きな概念と認識しています。
モックやパッチは、特に外部依存や副作用のあるコードをテストする際に重要な役割を果たします。モックやパッチを利用する主な目的を下記に列挙します。
- 外部依存の排除と副作用の制御
- APIやデータベースといった外部リソースへの依存を切り離し、テストを速く安定させることが可能。またテストによるファイルやデータ変更などの副作用(=期待しない動作)を防ぎ安全なテスト実行ができる
- 再現性、信頼性、品質の向上
- 常に同じ環境でテストでき、結果がブレずに再現性が高まるためコードの品質を確保できる
- 効率的なテストシナリオ構築
- エラーケースや特定条件のシミュレーションが容易になり、多様なケースを短時間でテストできる。
pytestで利用できる3つのモック
pytestで利用なモックとしてunittest.mock
、monkeypatch
、pytest-mock
があります。インターネット上の情報源ではなかなかこの3つの機能が整理されているものがなく、改めて特徴を下記にまとめたいと思います。
- unittest.mock
- Python標準ライブラリ(pytestをインストールせずとも利用可能)
- モックやパッチの実装が可能。環境変数の置き換えの実装は少し複雑。
- monkeypatch
- pytest組み込みモジュール
- 簡単なパッチ処理の実装や環境変数や動的に変わる設定の変更をする場合に利用。
- pytest-mock
- pytest-mockプラグイン(pytestとは別にインストールが必要)
unittest.mock
の機能をpytestのフィクスチャとして利用でき、patch
をpytestのテストに簡単に組み込みたい場合に便利。unittest.mockと同様、環境変数の置き換えは少し複雑。
使い分けについて悩むところですが、私としてはpytestを利用しているならば下記のいずれかかなと思います。理由としては利用する機能をpytestに限定するためです。
- パターン1:patchの実装がある程度複雑である場合は
pytest-mock
とmonkeypatch
の組み合わせ - パターン2:patchの実装が容易である場合は
monkeypatch
でなるべく実装
一方、インターネットや書籍ではunittest.mock
に関する情報も多く提供されています。そのため実際に実装をすることはなかったとしてもunittest.mock
の実装方法を理解しておくとよいでしょう。
実装例
unittest.mock
下記のコードではpatch
を使用して fetch_data
関数を「モック」し、一時的に返り値を "mocked data"
に変更しています。この場合、 fetch_data
の元の実装は一時的に置き換えられます。モックが有効なのはwithブロックのみになります。
from unittest.mock import patch
# テスト対象の関数
def fetch_data():
return "original data"
# パッチを適用したテスト
def test_fetch_data():
with patch("__main__.fetch_data", return_value="mocked data"):
result = fetch_data()
assert result == "mocked data"
monkeypatch
monkeypatch
を利用するためにはpytestをインストールする必要があります。
pip install pytest
下記のコードではmonkeypatch.setenv
を利用して、環境変数 MY_ENV_VAR
の値を一時的に mocked_value
に変更しています。テストが終わると、環境変数は元の状態に戻ります。
# 環境変数の設定変更例
import os
def get_env_var():
return os.getenv("MY_ENV_VAR", "default")
def test_get_env_var(monkeypatch):
# `os.getenv` を変更し、環境変数をテスト環境に置き換える
monkeypatch.setenv("MY_ENV_VAR", "mocked_value")
assert get_env_var() == "mocked_value"
pytest-mock
pytest-mock
を利用するためにはpytestに加え、pytest-mock
も必要です。
pip install pytest pytest-mock
pytest-mock
を利用する上で大事なのがmocker
です。mocker
は、pytest-mock
をインストールすると自動的に利用可能になるpytestのフィクスチャです。このフィクスチャを使うことで、unittest.mock
ライブラリを直接インポートせずにモック機能を利用できます。pytestの標準機能として提供されるので、テスト関数に引数としてmocker
を指定するだけで、モック操作ができるようになります。
関数のモック
まず関数のモックを見ていきます。テスト対象はtarget.py
、テストはtest_target.py
です。
# target.py
import requests
def fetch_data(url):
response = requests.get(url)
return response.json()
下記のテストコードですが、5行目ではmocker.patch
でrequests.getをモックし外部リクエストをしないようにしています。続いて6行目では mock_get
の戻り値をjsonメソッドが返す値に変更しています。そして10行目でテストの評価を行っています。さらに11行目では assert_called_once_with
でrequests.getが一度だけ指定のURLで呼び出されたことを確認することができ、呼び出しの検証も同時に行えます。
# test_target.py
from target import fetch_data
def test_fetch_data(mocker):
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"key": "value"}
result = fetch_data("http://example.com")
assert result == {"key": "value"}
mock_get.assert_called_once_with("http://example.com")
クラスのモック
次にクラスのモックを見ていきます。テスト対象はmymodule.py
、テストはtest_mymodule.py
です。
クラスのメソッドをモックする場合は特定のメソッドを一時的に置き換えてその動作を確認できるようにします。これによりオリジナルのメソッドの動作を再現したり、意図した結果を返すように設定したりすることができます。
# mymodule.py
class MyClass:
def method_to_mock(self):
return "Original Value"
def use_method():
obj = MyClass()
return obj.method_to_mock()
下記のテストコードですが、5行目でMyClass.method_to_mock
メソッドをモックし、このメソッドが呼び出されると"Mocked Value"
が返されるように設定しています。7行目のuse_method
の処理では上記のmymodule.py
にあるの実際のmethod_to_mock
ではなくモックされたメソッドを呼び出します。そのため、"Original Value"
ではなくモックで設定した"Mocked Value"
が返されます。
# test_mymodule.py
from mymodule import use_method
def test_use_method(mocker):
mocker.patch("mymodule.MyClass.method_to_mock", return_value="Mocked Value")
result = use_method()
assert result == "Mocked Value"
まとめ
本記事ではpytestで利用できる3つのモック機能について解説しました。まずは3つのツールの使い分けを理解した上で適切な場面でツールを選定できることが重要です。本記事を利用して3つのツールの利用シーンを理解してぜひ現場で使ってみてくださいね!