Traffine I/O

日本語

2023-02-17

unittest patch

はじめに

この記事では、Pythonの unittestpatchの詳細な使い方について紹介します。

patch

Pythonのunittestのpatch()メソッドは、モックオブジェクトを生成して、指定されたパッチ先オブジェクトの参照を入れ替えます。主要な引数は次のとおりです。

引数 説明
target パッチするオブジェクト。文字列形式でモジュール名を指定するか、オブジェクト自体を指定する。
new targetを置き換えるオブジェクト。省略可能。
side_effect モックが呼び出されたときに発生させる例外、返す値、または呼び出し可能オブジェクトを定義する関数。省略可能。
return_value モックの呼び出し時に返される値。省略可能。
autospec Trueの場合、newオブジェクトがtargetオブジェクトのスペックをまさしく満たしていることを保証する。
spec モックのスペックオブジェクトを指定する。省略可能。
create モックの呼び出しに対して自動的に属性を作成するかどうかを指定するブール値。省略可能で、デフォルトはFalse
new_callable newが呼び出し可能なオブジェクトでない場合、呼び出し可能なオブジェクトを作成するための呼び出し可能オブジェクトを指定する。
unsafe パッチング対象がモジュールの場合、False を指定すると検証が行われます。省略可能で、デフォルトはTrue

new

patchメソッドのnew引数を使用すると、元のオブジェクトを完全に置き換えることができます。この引数を使用すると、モックオブジェクトとして完全な新しいオブジェクトを作成できます。以下は、new引数を使用してFooオブジェクトを置き換える例です。

python
from unittest.mock import patch

class Foo:
    def bar(self):
        return "original"

with patch('__main__.Foo', new=lambda: "mock"):
    f = Foo()
    assert f.bar() == "mock"

この例では、Fooクラスのインスタンスに対してbar()メソッドを呼び出したときに"mock"が返されます。つまり、元のオブジェクトがモックオブジェクトに置き換えられます。

グローバル変数にダミーの値をセットすることも可能です。

python
from unittest.mock import patch

def test_my_global_variable():
    with patch('my_module.my_global_variable', new='dummy'):
        assert my_module.my_global_variable == 'dummy'

この例では、my_module.my_global_variableを文字列'dummy'に置き換えています。new引数は、グローバル変数に対して、与えられた新しい値をセットします。

return_value

return_value引数を使用すると、モックが呼び出されたときに返される値を指定できます。次の例では、my_functionが呼び出されたときにreturn_valueで指定した値を返すモックを作成しています。

python
from unittest.mock import patch

def my_function():
    return 42

with patch('__main__.my_function', return_value=84):
    result = my_function()
    print(result)  # 84

wraps

wraps引数を使用すると、モックがラップするオブジェクトの振る舞いを模倣できます。つまり、ラップするオブジェクトが持つ属性やメソッドを、モックも持つようになります。次の例では、my_functionが呼び出されたときに、本来のmy_functionを実行するモックを作成しています。

python
from unittest.mock import patch

def my_function():
    return 42

with patch('__main__.my_function', wraps=my_function):
    result = my_function()
    print(result)  # 42

side_effect

side_effect引数を使用すると、モックが呼び出されたときに発生する例外を指定できます。次の例では、my_functionが呼び出されたときにValueErrorを発生させるモックを作成しています。

python
from unittest.mock import patch

def my_function():
    return 42

with patch('__main__.my_function', side_effect=ValueError('oops')):
    result = my_function()
    print(result)  # ValueError: oops

patch.object

patch.object()は、指定されたオブジェクトの属性をモックオブジェクトに置き換えるために使用されるunittest.mockのメソッドです。このメソッドを使用すると、クラスのインスタンス属性やグローバル変数など、モジュール内の任意のオブジェクトの属性を置き換えることができます。これにより、テストが特定の状態で実行されるように設定できます。

patch.object()の構文は次のとおりです。

python
patch.object(target, attribute, new=DEFAULT, **kwargs)
引数 説明
target パッチするオブジェクトを指定する。
attribute パッチする属性の名前を指定する。
new オブジェクトの新しい値を指定する。デフォルト値はDEFAULTであり、これによりモックオブジェクトが自動的に作成される。
**kwargs 追加のパッチング引数を指定するために使用される。

以下は、patch.object()を使用した例です。

python
from unittest.mock import patch

class MyClass:
    def __init__(self):
        self.x = 10

def my_function():
    obj = MyClass()
    return obj.x

with patch.object(MyClass, 'x', 20):
    assert my_function() == 20

この例では、MyClassのインスタンス属性xをパッチして、20に設定しています。my_function()MyClassのインスタンスを作成し、xを返すため、 my_function()の戻り値は20になります。

patch.objectの他の例としては次のようなものがあります。

  • オブジェクトのメソッドを置き換える
python
from unittest.mock import patch

class MyClass:
    def my_method(self):
        return "original"

with patch.object(MyClass, "my_method", return_value="mocked"):
    obj = MyClass()
    result = obj.my_method()
    print(result)  # => "mocked"
  • モジュールの属性を置き換える
python
from unittest.mock import patch

import my_module

with patch.object(my_module, "my_function", return_value="mocked"):
    result = my_module.my_function()
    print(result)  # => "mocked"
  • 例外を発生させる
python
from unittest.mock import patch

def my_function():
    raise ValueError("original")

with patch.object(__main__, "my_function", side_effect=RuntimeError("mocked")):
    with pytest.raises(RuntimeError, match="mocked"):
        my_function()

patch との違い

patchpatch.objectは、両方ともモックの作成と設定を行うためのunittest.mockのメソッドですが、使用方法に違いがあります。

patchは、モック対象のオブジェクトを指定する文字列形式のパスまたはオブジェクトを引数に取ります。モック対象のオブジェクトがクラスメソッドである場合、第1引数に渡された文字列に含まれるクラス名をモックし、モックオブジェクトに置き換えます。また、patchは、withステートメント内で使用することを前提としています。

一方、patch.objectは、モック対象のオブジェクトと、そのオブジェクトの属性を指定する文字列を引数に取ります。モック対象のオブジェクトがクラスメソッドである場合でも、第1引数に渡されたオブジェクト自体がモックされ、属性の置き換えに使用されます。patch.objectは、withステートメント内で使用する必要はありません。

したがって、patch.objectを使用すると、オブジェクトの特定の属性のみをモックすることができますが、patchを使用すると、オブジェクト全体をモックすることができます。

例えば、次のようなコードがあるとします。

python
class MyClass:
    def my_method(self):
        return 'real'

my_instance = MyClass()

次のようにpatchを使用して、MyClass全体をモックすることができます。

python
from unittest.mock import patch

with patch('my_module.MyClass') as MockClass:
    instance = MockClass.return_value
    instance.my_method.return_value = 'mocked'
    assert my_instance.my_method() == 'mocked'

一方、次のようにpatch.objectを使用して、MyClassmy_method属性のみをモックすることができます。

python
from unittest.mock import patch

with patch.object(my_instance, 'my_method') as mock_method:
    mock_method.return_value = 'mocked'
    assert my_instance.my_method() == 'mocked'

デコレータと with 文

Pythonのunittestのpatchにおいて、デコレータとwith文を使用することは、基本的に同じ効果がありますが、書き方に違いがあります。

デコレータを使用する場合は、テストケース内の全てのテストメソッドでモックを有効にすることができます。以下は、デコレータを使用した例です。

python
import unittest
from unittest.mock import patch

class MyTestCase(unittest.TestCase):
    @patch('mymodule.some_function')
    def test_my_function(self, mock_some_function):
        # test ode with mocked function

with文を使用する場合は、with文内だけでモックを有効にすることができます。以下は、with文を使用した例です。

python
import unittest
from unittest.mock import patch

class MyTestCase(unittest.TestCase):
    def test_my_function(self):
        with patch('mymodule.some_function') as mock_some_function:
            # test code with mocked function

一般的に、デコレータを使用すると、コードが簡潔になります。しかし、テストメソッドごとにモックをカスタマイズする必要がある場合には、with文を使用することができます。また、with文を使用する場合は、withブロックの終了時に自動的にモックがクリーンアップされるため、テスト中に何かが失敗した場合でもクリーンアップが行われます。

複数の patch を定義

複数のpatchを定義する例を以下に示します。

例えば、次のようなモジュールがあるとします。

my_module.py

import os

def my_function():
    return os.path.abspath(__file__)

このモジュールに対して、複数のパッチを定義する場合、次のようにします。

python
import unittest.mock
import my_module

class TestMyModule(unittest.TestCase):
    def test_my_function(self):
        with unittest.mock.patch('my_module.os'):
            with unittest.mock.patch('my_module.os.path.abspath') as abspath_mock:
                abspath_mock.return_value = 'my/absolute/path'
                result = my_module.my_function()
                self.assertEqual(result, 'my/absolute/path')

この例では、osモジュールとそのpath.abspath関数に対してパッチを適用しています。パッチを適用する際には、unittest.mock.patch()メソッドを使用し、引数にパッチを適用したいオブジェクトのパスを指定します。パッチを複数適用する場合は、入れ子のwith文を使用します。また、パッチオブジェクトはas句を使ってエイリアスをつけることができます。ここではabspath_mockというエイリアスをつけ、return_value属性を使用してモックの戻り値を設定しています。

複数のpatchデコレータを定義する場合は、次のようにします。

python
from unittest.mock import patch

@patch('module1.function1')
@patch('module2.function2')
def test_my_function(mock_function2, mock_function1):
    # test code
    pass

この例では、module1.function1module2.function2をパッチするために、2つのpatchデコレータを使用しています。この場合、テスト関数の引数の順序は、デコレータの順序と一致するように注意してください。つまり、mock_function1module1.function1のモックであり、mock_function2module2.function2のモックです。

参考

https://docs.python.org/3/library/unittest.mock.html#the-patchers

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!