はじめに
この記事では、Pythonの unittest のpatchの詳細な使い方について紹介します。
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オブジェクトを置き換える例です。
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"が返されます。つまり、元のオブジェクトがモックオブジェクトに置き換えられます。
グローバル変数にダミーの値をセットすることも可能です。
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で指定した値を返すモックを作成しています。
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を実行するモックを作成しています。
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を発生させるモックを作成しています。
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()の構文は次のとおりです。
patch.object(target, attribute, new=DEFAULT, **kwargs)
| 引数 | 説明 |
|---|---|
target |
パッチするオブジェクトを指定する。 |
attribute |
パッチする属性の名前を指定する。 |
new |
オブジェクトの新しい値を指定する。デフォルト値はDEFAULTであり、これによりモックオブジェクトが自動的に作成される。 |
**kwargs |
追加のパッチング引数を指定するために使用される。 |
以下は、patch.object()を使用した例です。
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の他の例としては次のようなものがあります。
- オブジェクトのメソッドを置き換える
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"
- モジュールの属性を置き換える
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"
- 例外を発生させる
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 との違い
patchとpatch.objectは、両方ともモックの作成と設定を行うためのunittest.mockのメソッドですが、使用方法に違いがあります。
patchは、モック対象のオブジェクトを指定する文字列形式のパスまたはオブジェクトを引数に取ります。モック対象のオブジェクトがクラスメソッドである場合、第1引数に渡された文字列に含まれるクラス名をモックし、モックオブジェクトに置き換えます。また、patchは、withステートメント内で使用することを前提としています。
一方、patch.objectは、モック対象のオブジェクトと、そのオブジェクトの属性を指定する文字列を引数に取ります。モック対象のオブジェクトがクラスメソッドである場合でも、第1引数に渡されたオブジェクト自体がモックされ、属性の置き換えに使用されます。patch.objectは、withステートメント内で使用する必要はありません。
したがって、patch.objectを使用すると、オブジェクトの特定の属性のみをモックすることができますが、patchを使用すると、オブジェクト全体をモックすることができます。
例えば、次のようなコードがあるとします。
class MyClass:
def my_method(self):
return 'real'
my_instance = MyClass()
次のようにpatchを使用して、MyClass全体をモックすることができます。
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を使用して、MyClassのmy_method属性のみをモックすることができます。
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文を使用することは、基本的に同じ効果がありますが、書き方に違いがあります。
デコレータを使用する場合は、テストケース内の全てのテストメソッドでモックを有効にすることができます。以下は、デコレータを使用した例です。
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文を使用した例です。
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を定義する例を以下に示します。
例えば、次のようなモジュールがあるとします。
import os
def my_function():
return os.path.abspath(__file__)
このモジュールに対して、複数のパッチを定義する場合、次のようにします。
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デコレータを定義する場合は、次のようにします。
from unittest.mock import patch
@patch('module1.function1')
@patch('module2.function2')
def test_my_function(mock_function2, mock_function1):
# test code
pass
この例では、module1.function1とmodule2.function2をパッチするために、2つのpatchデコレータを使用しています。この場合、テスト関数の引数の順序は、デコレータの順序と一致するように注意してください。つまり、mock_function1はmodule1.function1のモックであり、mock_function2はmodule2.function2のモックです。
参考