LoginSignup
1
0

More than 1 year has passed since last update.

mocker fixureを使用したテスト対象処理のMock化

Posted at

本記事について

本記事では、ユニットテストの対象となるメソッド内で呼び出している各種メソッドを、pytestのmocker fixureを使用してpatchを適用し、テスト対象となる処理をMock化する場合の実装サンプルを記載しています。

本記事の作成環境

  • Ubuntu 20.04.3 LTS on WSL2
  • Python: 3.9.7
  • pytest: 7.1.2
  • pytest-mock: 3.7.0

patchの適用によるテスト対象のMock化

クラスオブジェクトに対してpatchを適用

クラスオブジェクトに対してmocker.patch()、またはmocker.patch.object()を使用して、patchを適用します。
クラスオブジェクトにpatchを適用するため、テストメソッドを終了するまでは、以降は常にMock化した値を返却します。

クラスメソッド / 静的メソッド

テスト対象メソッド

pathlib.Pathクラスのクラスメソッドである、cwd()にpatchを適用してテストを実施します。
本手順ではクラスメソッドにpatchを適用していますが、静的メソッドに対しても同様の手順でpatchの適用が可能です。

sample/foo.py
from pathlib import Path

class Foo:
    @staticmethod
    def get_current_directory():
        return Path.cwd()

クラスオブジェクトを指定してpatchを適用

pathlib.Pathクラスオブジェクトのcwd()メソッドをtargetに指定してpatchを適用します。
オブジェクトを対象とするため、mocker.patch.object()を使用します。

tests/test_foo.py
# pathlib.Pathにpatchを適用するため、Pathのimportが必要
from pathlib import Path
from sample.foo import Foo

def test_get_current_directory(mocker):
    path = Path("current_dir/")
    # Pathクラスオブジェクトのcwd()にpatchを適用
    mocker.patch.object(Path, "cwd", return_value=path)

    assert Foo.get_current_directory() == path

メソッドを指定してpatchを適用

pathlib.Path.cwd()メソッドをtargetに指定してpatchを適用します。
メソッドを対象とするため、mocker.patch()を使用します。

tests/test_foo.py
from sample.foo import Foo

def test_get_current_directory(mocker):
    path = Path("current_dir/")
    # sample.fooモジュールにimportされているPath.cwd()にpatchを適用
    mocker.patch("sample.foo.Path.cwd", return_value=path)

    assert Foo.get_current_directory() == path

インスタンスメソッド

pathlib.Pathクラスのインスタンスメソッドである、is_file()にpatchを適用してテストを実施します。

テスト対象メソッド

sample/foo.py
from pathlib import Path

class Foo:
    def __init__(self):
        self._work_directory = Path("./work")
        self._work_file = self._work_directory / Path("work_file")

    def has_work_file(self):
        return self._work_file.is_file()

クラスオブジェクトを指定してpatchを適用

pathlib.Pathクラスオブジェクトのis_file()メソッドをtargetに指定してpatchを適用します。
オブジェクトを対象とするため、mocker.patch.object()を使用します。

tests/test_foo.py
# pathlib.Pathにpatchを適用するため、Pathのimportが必要
from pathlib import Path
from sample.foo import Foo

def test_has_file(mocker):
    # Pathクラスオブジェクトのis_file()にpatchを適用
    mocker.patch.object(Path, "is_file", return_value=True)

    foo = Foo()
    assert foo.has_work_file() == True

メソッドを指定してpatchを適用

pathlib.Path.is_file()メソッドをtargetに指定してpatchを適用します。
メソッドを対象とするため、mocker.patch()を使用します。

tests/test_foo.py
from sample.foo import Foo

def test_has_file(mocker):
    foo = Foo()

    # sample.fooモジュールにimportされているPath.is_file()にpatchを適用
    mocker.patch("sample.foo.Path.is_file", return_value=True)

    assert foo.has_work_file() == True

プロパティ

pathlib.Pathクラスのプロパティである、nameにpatchを適用してテストを実施します。

sample/foo.py
from pathlib import Path

class Foo:
    def __init__(self):
        self._work_directory = Path("./work")
        self._work_file = self._work_directory / Path("work_file")

    def get_work_file_name(self):
        return self._work_file.name

クラスオブジェクトを指定してpatchを適用

pathlib.Pathクラスオブジェクトのnameプロパティをtargetに指定してpatchを適用します。
オブジェクトを対象とするため、mocker.patch.object()を使用します。
その際、プロパティに対してpatchを適用するため、new_callablePropertyMockを渡します。

tests/test_foo.py
# pathlib.Pathにpatchを適用するため、Pathのimportが必要
from pathlib import Path
from sample.foo import Foo

def test_get_work_file_name(mocker):
    foo = Foo()

    # Pathクラスオブジェクトのis_file()にpatchを適用
    # プロパティ
    mocker.patch.object(
            Path,
            "name",
            new_callable=mocker.PropertyMock,
            return_value="mocked_work_file")

    assert foo.get_work_file_name() == "mocked_work_file"

インスタンスオブジェクトに対してpatchを適用

インスタンスオブジェクトに対しても、クラスオブジェクトと同様にmocker.patch()、またはmocker.patch.object()を使用してpatchを適用することが可能です。
しかし、実際にインスタンスオブジェクトに対してpatchを適用しようとすると、エラーが発生してpatchを適用できない場合があります。1
その場合は、patchを適用するインスタンスオブジェクトをMockに置き換え、patchを適用します。
インスタンスオブジェクトをMockに置き換えているため、そのインスタンスオブジェクトのみMock化した値を返却します。

インスタンスメソッド

pathlib.Pathクラスのインスタンスメソッドである、is_file()にpatchを適用してテストを実施します。

テスト対象メソッド

sample/foo.py
from pathlib import Path

class Foo:
    def __init__(self):
        self._work_directory = Path("./work")
        self._work_file = self._work_directory / Path("work_file")

    def has_work_file(self):
        return self._work_file.is_file()

インスタンスオブジェクトをMockに置き換えてpatchを適用

テストでis_file()メソッドを呼び出すpathlib.PathインスタンスオブジェクトをMagicMockに置き換え、置き換えたMockのis_file()メソッドをtargetに指定してpatchを適用します。
オブジェクトを対象とするため、mocker.patch.object()を使用します。

tests/test_foo.py
from sample.foo import Foo

def test_has_file(mocker):
    foo = Foo()

    # foo._work_fileをMagicMockに置き換え、is_file()にpatchを適用
    foo._work_file = mocker.MagicMock()
    mocker.patch.object(
            foo._work_file,
            "is_file",
            return_value=True)

    assert foo.has_work_file() == True

プロパティ

pathlib.Pathクラスのプロパティである、nameにpatchを適用してテストを実施します。

sample/foo.py
from pathlib import Path

class Foo:
    def __init__(self):
        self._work_directory = Path("./work")
        self._work_file = self._work_directory / Path("work_file")

    def get_work_file_name(self):
        return self._work_file.name

インスタンスオブジェクトのプロパティをPropertyMockに置き換え

テストでnameプロパティを呼び出すpathlib.PathインスタンスオブジェクトをMagicMockに置き換え、置き換えたMockのnameプロパティにPropertyMockを設定します。

tests/test_foo.py
from sample.foo import Foo

def test_get_work_file_name(mocker):
    foo = Foo()

    # foo._work_fileをMagicMockに置き換え
    foo._work_file = mocker.MagicMock()
    # foo._work_fileのnameプロパティにPropertyMockを設定
    type(foo._work_file).name = mocker.PropertyMock(return_value="mocked_value")

    assert foo.get_work_file_name() == "mocked_value"

インスタンス生成操作

インスタンスオブジェクトをテスト対象メソッド内で生成している場合は、代入によりpatchを適用するインスタンスオブジェクトをMockに置き換えることができません。
この場合は、インスタンス生成操作に対してMockを返却するようにpatchを適用します。
patchはクラスオブジェクトに対して適用されるため、テストメソッドを終了するまでは、以降に生成するすべてのインスタンスはMockとなります。

テスト対象メソッド

sample/foo.py
from pathlib import Path

class Foo:
    @staticmethod
    def is_file_exist(path):
        # pathlib.Pathクラスのインスタンスオブジェクトを生成し、is_file()を呼び出し
        return Path(path).is_file()

インスタンス生成操作を指定してpatchを適用

pathlib.Pathクラスのインスタンス生成操作をtargetに指定してpatchを適用します。
インスタンス生成操作を対象とするため、mocker.patch()を使用します。

tests/test_foo.py
from sample.foo import Foo

def test_is_file_exist(mocker):
    # Path.is_file()のpatchを適用したMagicMockを作成
    mocked_path = mocker.MagicMock()
    mocker.patch.object(mocked_path, "is_file", return_value=True)

    # sample.fooモジュールにimportされているPathインスタンスオブジェクトの生成操作にpatchを適用
    # インスタンス生成操作はPathクラスのメソッドではないため、mocker.patch.object() では適用できない
    mocker.patch("sample.foo.Path", return_value=mocked_path)

    assert Foo.is_file_exist("/path/to/file/file_name") == True

参考

  1. patchを適用するクラスが__slots__を定義している場合は、patchを適用するメソッド(属性)がread onlyとなるため、patchを適用できない。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0