pytestで利用できるmock、monkeypatch、pytest-mockを理解しよう!

eyecatch-pytest-mock-monkeypatch-pytest-mock
目次

概要

pytest

pytestは、Python 用のシンプルで柔軟なテストフレームワークです。環境構築が容易で書きやすく、Pythonのテストフレームワークでファーストチョイスになるツールです。本記事ではpytestで利用できるモックについて説明をします。もしpytestについておさらいしたい方や基本を確認したい方は下記の記事をご参考ください。

モックとパッチ

一般にモックはオブジェクトそのもののダミーを作成する行為やそのオブジェクトを指し、パッチはそのモックやダミーオブジェクトを適用して元のオブジェクトを一時的に置き換える行為を指します。私の認識ではモックはパッチを含む大きな概念と認識しています。

モックやパッチは、特に外部依存や副作用のあるコードをテストする際に重要な役割を果たします。モックやパッチを利用する主な目的を下記に列挙します。

  • 外部依存の排除と副作用の制御
    • APIやデータベースといった外部リソースへの依存を切り離し、テストを速く安定させることが可能。またテストによるファイルやデータ変更などの副作用(=期待しない動作)を防ぎ安全なテスト実行ができる
  • 再現性、信頼性、品質の向上
    • 常に同じ環境でテストでき、結果がブレずに再現性が高まるためコードの品質を確保できる
  • 効率的なテストシナリオ構築
    • エラーケースや特定条件のシミュレーションが容易になり、多様なケースを短時間でテストできる。

pytestで利用できる3つのモック

pytestで利用なモックとしてunittest.mockmonkeypatchpytest-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-mockmonkeypatchの組み合わせ
  • パターン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つのツールの利用シーンを理解してぜひ現場で使ってみてくださいね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Hack Luck Labの管理人hakula(ハクラ)です。2012年にSIerに新卒入社し、SE、新規事業、情シスを担当。その後、ITコンサルを経て、現在はバックエンドエンジニア。過去にはC#、SQL Server、JavaScriptで開発を行い、現在はPython、Rest Framework、Postgresql、Linux、AWSなどを使用しています。ノーコードツールやDX関連も興味あり。「技術は価値を生むために使う」ことが信条で、顧客や組織への貢献を重視しています。

目次