pytest で monkeypatch を利用する

pytest で monkeypatch する方法を確認します。

下記は公式サイトの紹介ページ。

How to monkeypatch/mock modules and environments

リファレンス。

monkeypatch

環境

Python と pytest 周りで mock する方法について

pytest の monkeypatch の話へはいる前に、自分の中での情報整理のため、Python と pytest 周りで mock に利用できるライブラリや機能について整理します。

名前 説明
unittest.mock Python の標準モジュール
テストコードで unittest を利用する場合に利用
mock Python の 3rd Party Library
であるが、Python 3.3 以降は unittest.mock に取り込まれている
monkeypatch pytest の組み込み fixture
pytest-mock pytest の 3rd Party Plug-in
mock の wrapper として機能する pytest の fixture を提供する、らしい

サンプルコード

ディレクトリ構造

.
├── src
│   ├── __init__.py
│   └── sample.py
└── tests
    ├── __init__.py
    └── test.py

テスト対象のコード

./src/sample.py

import os
from typing import Optional


class Sample:
    DEFAULT_CONFIG = {"version": "1.0"}

    def __init__(self, number: int):
        self.__number = number

    def get_version(self) -> str:
        return f"VERSION :{Sample.DEFAULT_CONFIG['version']}"

    def get_os_user(self) -> Optional[str]:
        return os.getenv("USER")

    def double(self) -> int:
        return self.__number * 2

    def get_version_os_user(self) -> str:
        return f"VERSION :{Sample.DEFAULT_CONFIG['version']}, USER: {os.getenv('USER')}"  # noqa: E501

テストコード

./tests/test.py

monkeypatch という fixture を呼んだ関数内で、mock 用のメソッドを利用します。且つ monkeypatch を利用している関数を fixture とすることもでき、テスト用の関数で利用できます。

import pytest
from src.sample import Sample


def test_get_version(monkeypatch):
    """辞書の上書き/削除
       monkeypatch.setitem(mapping, name, value)
       monkeypatch.delitem(obj, name, raising=True)
    """
    monkeypatch.setitem(Sample.DEFAULT_CONFIG, "version", "1.1")
    sample = Sample(2)
    assert sample.get_version() == "VERSION :1.1"


def test_get_os_user(monkeypatch):
    """環境変数の上書き/削除
       monkeypatch.setenv(name, value, prepend=None)
       monkeypatch.delenv(name, raising=True)
    """
    monkeypatch.setenv("USER", "sui-chan")
    sample = Sample(2)
    assert sample.get_os_user() == "sui-chan"


def test_double(monkeypatch):
    """メソッドの上書き/削除
       monkeypatch.setattr(obj, name, value, raising=True)
       monkeypatch.delattr(obj, name, raising=True)
    """
    monkeypatch.setattr(Sample, "double", lambda x: 10)
    sample = Sample(2)
    assert sample.double() == 10


@pytest.fixture
def patch_version(monkeypatch):
    """monkeypatchを利用した関数を fixture として定義"""
    monkeypatch.setitem(Sample.DEFAULT_CONFIG, "version", "2.0")


@pytest.fixture
def patch_user(monkeypatch):
    """monkeypatchを利用した関数を fixture として定義"""
    monkeypatch.setenv("USER", "mikochi")


def test_get_version_os_user(patch_version, patch_user):
    """上記で fixture とした関数を利用"""
    sample = Sample(2)
    assert sample.get_version_os_user() == "VERSION :2.0, USER: mikochi"

実行コマンド

$ pytest -v tests/test.py 
============================== test session starts ==============================
collected 4 items                                                                                                                                                                                                    

tests/test.py::test_get_version PASSED                                      [ 25%]
tests/test.py::test_get_os_user PASSED                                      [ 50%]
tests/test.py::test_double PASSED                                           [ 75%]
tests/test.py::test_get_version_os_user PASSED                              [100%]

============================== 4 passed in 0.02s ================================

その他

サンプルコード内で利用していないものでは、下記があります。

  • monkeypatch.syspath_prepend(path: list)
    • Python の sys.path の値を上書きする
  • monkeypatch.chdir(path: Union[str, "os.PathLike[str]"])