Pythonのテストを加速する!unittest.mock 詳細解説

はじめに: なぜモックが必要なのか?

ソフトウェア開発において、テストは品質を担保するために不可欠なプロセスです。しかし、現実世界のシステムは複雑で、多くの外部依存関係(データベース、API、ファイルシステムなど)を持っています。これらの依存関係は、テストの実行を遅くしたり、不安定にしたり、あるいは実行そのものを困難にする場合があります。

例えば、外部APIを呼び出す機能をテストする場合、毎回実際のAPIを呼び出すのは非効率的ですし、ネットワークの問題やAPI側の都合でテストが失敗する可能性もあります。また、特定の条件下でのみ発生するエラーケースを再現するのも難しいでしょう。

ここで活躍するのがモック (Mock) です。モックとは、テスト対象のコードが依存しているオブジェクトや関数を、本物の代わりに模倣する「偽物」のオブジェクトです。モックを使うことで、以下のようなメリットがあります。

  • テストの分離: テスト対象のコードを、その依存関係から切り離し、ユニットテストを純粋なものに保てます。
  • テストの高速化: 時間のかかる処理(ネットワーク通信、DBアクセスなど)を模倣することで、テストの実行時間を大幅に短縮できます。
  • テストの安定化: 外部要因によるテストの失敗を防ぎ、安定したテスト結果を得られます。
  • エッジケースのテスト: 例外発生や特定の値の返却など、再現が難しい状況を簡単にシミュレートできます。

Pythonでは、標準ライブラリの unittest.mock モジュールが強力なモック機能を提供しています。(Python 3.3より前のバージョンでは、mock というサードパーティライブラリとして提供されていましたが、Python 3.3以降で標準ライブラリに取り込まれました。)

このブログでは、unittest.mock の基本的な使い方から、より高度なテクニックまで、具体的なコード例を交えながら詳しく解説していきます。これを読めば、あなたもモックを使いこなし、より効率的で信頼性の高いテストを書けるようになるはずです!

基本のキ: Mockオブジェクトの作成と操作

unittest.mock の中心となるのが Mock クラスです。まずは、この Mock オブジェクトを作成し、基本的な操作を見ていきましょう。

from unittest.mock import Mock

# Mockオブジェクトの作成
mock_obj = Mock()

# 属性へのアクセス (存在しない属性でもエラーにならない)
print(mock_obj.some_attribute)
# 出力: <Mock name='mock.some_attribute' id='...'>

# メソッドの呼び出し (存在しないメソッドでもエラーにならない)
result = mock_obj.some_method(1, 'a', key='value')
print(result)
# 出力: <Mock name='mock.some_method()' id='...'>

# Mockオブジェクト自体も呼び出し可能
mock_obj()
# 出力: <Mock name='mock()' id='...'>

驚くべきことに、Mock オブジェクトは、存在しない属性にアクセスしたり、存在しないメソッドを呼び出したりしても、エラーを発生させません。代わりに、アクセスされた属性名やメソッド名を持つ新しい Mock オブジェクトを返します。これは非常に柔軟ですが、タイプミスなどに気づきにくいという側面もあります(後述する specautospec で対処できます)。

モックオブジェクトのメソッドが特定の値を返すように設定したり、属性に値を設定したりすることができます。

from unittest.mock import Mock

# Mockオブジェクトの作成
mock_api = Mock()

# メソッドの戻り値を設定
mock_api.get_data.return_value = {'id': 1, 'name': 'Test Data'}

# 属性値を設定
mock_api.api_key = 'dummy_key_123'

# 設定した戻り値が返されることを確認
data = mock_api.get_data()
print(data)
# 出力: {'id': 1, 'name': 'Test Data'}

# 設定した属性値が取得できることを確認
print(mock_api.api_key)
# 出力: dummy_key_123

# 未設定のメソッド呼び出しはデフォルトのMockオブジェクトを返す
print(mock_api.another_method())
# 出力: <Mock name='mock.another_method()' id='...'>

return_value 属性を使うことで、メソッド呼び出し時の戻り値を指定できます。また、通常の属性と同じように値を代入することで、モックオブジェクトの属性を設定できます。

メソッドが呼び出されるたびに異なる値を返したり、例外を発生させたりするなど、より複雑な振る舞いをさせたい場合は side_effect 属性を使用します。

from unittest.mock import Mock

mock_func = Mock()

# side_effect にイテラブルを指定すると、呼び出すたびにその要素を返す
mock_func.side_effect = [1, 2, ValueError("Something went wrong!")]

print(mock_func())
# 出力: 1
print(mock_func())
# 出力: 2

try:
    mock_func()
except ValueError as e:
    print(e)
# 出力: Something went wrong!

# side_effect に関数を指定すると、モックが呼び出されたときにその関数が実行される
def custom_side_effect(*args, **kwargs):
    print(f"Called with args: {args}, kwargs: {kwargs}")
    if kwargs.get("raise_error"):
        raise TypeError("Custom type error")
    return "Processed"

mock_complex = Mock()
mock_complex.side_effect = custom_side_effect

print(mock_complex(10, name="example"))
# 出力:
# Called with args: (10,), kwargs: {'name': 'example'}
# Processed

try:
    mock_complex(raise_error=True)
except TypeError as e:
    print(e)
# 出力:
# Called with args: (), kwargs: {'raise_error': True}
# Custom type error

side_effect は非常に強力で、以下のような設定が可能です。

  • イテラブル (リスト、タプルなど): 呼び出されるたびにイテラブルの次の要素を返します。要素がなくなると StopIteration が発生します。
  • 例外クラスまたはインスタンス: 呼び出されると指定された例外を発生させます。
  • 関数 (呼び出し可能オブジェクト): 呼び出されると、その関数が実行され、その関数の戻り値がモックの戻り値となります。関数にはモックが受け取った引数がそのまま渡されます。
  • None: side_effect の設定を解除します。

依存関係の差し替え: patch デコレータとコンテキストマネージャ

実際のテストでは、特定のモジュール内の関数やクラス、オブジェクトなどを一時的にモックに差し替えたい場合がほとんどです。unittest.mock.patch は、これを簡単に行うための強力なツールです。デコレータとしても、コンテキストマネージャとしても使用できます。

テスト関数やテストクラスのメソッドにデコレータとして適用します。デコレータで指定した対象が、関数の実行中だけモックオブジェクトに置き換えられます。モックオブジェクトは、テスト関数の引数として渡されます。

例として、os.path.exists をモック化してみましょう。

import os
from unittest.mock import patch
import unittest # unittest フレームワークを使う例

def check_file_existence(filepath):
    if os.path.exists(filepath):
        return f"File '{filepath}' exists."
    else:
        return f"File '{filepath}' does not exist."

class MyTestCase(unittest.TestCase):

    @patch('os.path.exists') # モック化する対象を文字列で指定
    def test_file_exists(self, mock_exists): # モックオブジェクトが引数として渡される
        # モックの設定: exists が True を返すようにする
        mock_exists.return_value = True

        result = check_file_existence("/path/to/real/or/fake/file.txt")
        self.assertEqual(result, "File '/path/to/real/or/fake/file.txt' exists.")

        # os.path.exists が期待通りに呼び出されたかを確認
        mock_exists.assert_called_once_with("/path/to/real/or/fake/file.txt")

    @patch('os.path.exists')
    def test_file_does_not_exist(self, mock_exists):
        # モックの設定: exists が False を返すようにする
        mock_exists.return_value = False

        result = check_file_existence("/another/path.log")
        self.assertEqual(result, "File '/another/path.log' does not exist.")

        mock_exists.assert_called_once_with("/another/path.log")

# unittestを実行する場合
# if __name__ == '__main__':
#     unittest.main()

@patch('os.path.exists') のように、モック化したい対象を文字列で指定します。この文字列は、対象がインポートされる際のパス(どこで使われているか)を指定する必要があります。例えば、my_module.py の中で import os して os.path.exists を使っている場合、パッチする対象は 'my_module.os.path.exists' になります。これは少し混乱しやすい点なので注意が必要です。通常は、テスト対象のモジュールから見たインポートパスを指定します。

複数のデコレータを使用する場合、引数は下から上への順序で渡されます。

from unittest.mock import patch
import unittest

class AnotherTestCase(unittest.TestCase):
    @patch('module1.func1') # 最後に適用される -> 最初の引数
    @patch('module2.ClassA') # 最初に適用される -> 最後の引数
    def test_multiple_patches(self, mock_class_a, mock_func1):
        # mock_class_a は module2.ClassA のモック
        # mock_func1 は module1.func1 のモック
        self.assertTrue(hasattr(mock_class_a, 'assert_called_once'))
        self.assertTrue(hasattr(mock_func1, 'assert_called_once'))

with 文を使って、特定のブロック内だけでモック化することもできます。

import os
from unittest.mock import patch
import unittest

def process_file(filepath):
    if os.path.exists(filepath):
        # 何らかの処理
        return "Processed"
    else:
        raise FileNotFoundError(f"File not found: {filepath}")

class ContextManagerTest(unittest.TestCase):
    def test_process_existing_file(self):
        # with ブロック内だけで os.path.exists がモック化される
        with patch('os.path.exists') as mock_exists:
            # モックの設定
            mock_exists.return_value = True

            result = process_file("dummy.txt")
            self.assertEqual(result, "Processed")
            mock_exists.assert_called_once_with("dummy.txt")

        # with ブロックを抜けると os.path.exists は元に戻る
        # print(os.path.exists) # 本物の関数に戻っている

    def test_process_non_existing_file(self):
        with patch('os.path.exists') as mock_exists:
            mock_exists.return_value = False

            with self.assertRaises(FileNotFoundError):
                process_file("non_existent.txt")
            mock_exists.assert_called_once_with("non_existent.txt")

コンテキストマネージャは、テスト関数全体ではなく、一部の処理だけをモック化したい場合に便利です。

特定のオブジェクトの属性やメソッドだけをモック化したい場合は、patch.object を使うと便利です。対象オブジェクトと、モック化したい属性名を指定します。

from unittest.mock import patch
import unittest

class SomeService:
    def get_user_data(self, user_id):
        # 実際にはDBアクセスなどを行う
        print(f"Fetching data for user {user_id} from real service...")
        return {'id': user_id, 'name': 'Real User'}

service_instance = SomeService()

class PatchObjectTest(unittest.TestCase):
    def test_service_with_mocked_method(self):
        user_id_to_test = 123
        mock_return_data = {'id': user_id_to_test, 'name': 'Mocked User'}

        # service_instance の get_user_data メソッドをモック化
        with patch.object(service_instance, 'get_user_data', return_value=mock_return_data) as mock_method:
            # service_instance のメソッドを呼び出すとモックが使われる
            data = service_instance.get_user_data(user_id_to_test)

            self.assertEqual(data, mock_return_data)
            mock_method.assert_called_once_with(user_id_to_test)

        # with ブロックを抜けると元のメソッドに戻る
        # real_data = service_instance.get_user_data(999)
        # print(real_data) # "Fetching data..." が表示され、実際の(または元の)処理が行われる

通常の Mockpatch では、存在しない属性やメソッドにアクセスしたり、間違った引数で呼び出したりしてもエラーになりません。これは便利ですが、テスト対象のインターフェース(API)の変更に気づきにくくなるという欠点があります。

ここで役立つのが autospec=True オプションです。patchMock の作成時にこのオプションを指定すると、モックは元のオブジェクトの仕様(属性、メソッド、引数シグネチャ)を引き継ぎます。これにより、元のオブジェクトに存在しない属性やメソッドにアクセスしようとしたり、間違った引数でメソッドを呼び出そうとしたりすると、エラーが発生するようになります。

from unittest.mock import patch, Mock
import unittest

class RealClass:
    def method(self, arg1, arg2):
        return f"Called with {arg1} and {arg2}"

    class_attribute = 100

class AutoSpecTest(unittest.TestCase):

    # autospec=True を使った patch
    @patch('__main__.RealClass', autospec=True)
    def test_method_call_with_autospec(self, MockRealClass):
        instance = MockRealClass()

        # 正しい引数で呼び出し
        instance.method('hello', 'world')
        instance.method.assert_called_once_with('hello', 'world')

        # 間違った引数で呼び出すと TypeError が発生する
        with self.assertRaises(TypeError):
            instance.method('too', 'many', 'args')

        # 存在しないメソッドを呼び出すと AttributeError が発生する
        with self.assertRaises(AttributeError):
            instance.non_existent_method()

        # クラス属性も模倣される (ただしMockオブジェクトになる)
        self.assertTrue(isinstance(MockRealClass.class_attribute, Mock))
        # print(MockRealClass.class_attribute) # <NonCallableMagicMock name='RealClass.class_attribute' ...>

    def test_mock_with_autospec(self):
        # Mock の作成時に autospec を使う例
        mock_instance = Mock(spec=RealClass) # spec= でも同様の効果

        # 正しい引数
        mock_instance.method('a', 'b')
        mock_instance.method.assert_called_once_with('a', 'b')

        # 間違った引数 -> TypeError
        with self.assertRaises(TypeError):
            mock_instance.method()

        # 存在しない属性 -> AttributeError
        with self.assertRaises(AttributeError):
            print(mock_instance.other_attribute)

        # spec_set= を使うと、存在しない属性への代入も禁止できる
        mock_strict = Mock(spec_set=RealClass)
        with self.assertRaises(AttributeError):
            mock_strict.new_attribute = "forbidden"

autospec=True (または spec=ClassNamespec_set=ClassName) を使うことで、より安全でリファクタリングに強いテストを書くことができます。特に理由がない限り、積極的に利用することをお勧めします。

マジックメソッドもお任せ: MagicMock

Pythonには、__str__, __len__, __getitem__ のような、ダブルアンダースコアで囲まれた特殊なメソッド(マジックメソッドやダンダーメソッドと呼ばれます)があります。通常の Mock オブジェクトは、デフォルトではこれらのマジックメソッドを模倣しません。

from unittest.mock import Mock

mock_obj = Mock()

# 通常のMockではマジックメソッドはデフォルトで用意されていない
# len(mock_obj) # TypeError: object of type 'Mock' has no len()
# mock_obj[0]   # TypeError: 'Mock' object is not subscriptable
# str(mock_obj) # デフォルトの文字列表現は返るが、モック化されていない

# 手動で設定することは可能
mock_obj.__len__.return_value = 5
print(len(mock_obj)) # 出力: 5

マジックメソッドも含めてモック化したい場合は、Mock のサブクラスである MagicMock を使用します。

from unittest.mock import MagicMock

magic_mock = MagicMock()

# MagicMock は多くの一般的なマジックメソッドをデフォルトでサポート
print(len(magic_mock))     # デフォルトの戻り値は 0 (場合による) もしくは設定可能
print(magic_mock[10])    # デフォルトでMagicMockを返す
print(str(magic_mock))     # デフォルトでそれらしい文字列を返す
print(int(magic_mock))     # デフォルトで 1 (場合による) もしくは設定可能

# もちろん、戻り値をカスタマイズできる
magic_mock.__str__.return_value = "Magic Mock String!"
magic_mock.__len__.return_value = 42
magic_mock.__getitem__.return_value = 'item value'
magic_mock.__int__.return_value = 99

print(str(magic_mock))    # 出力: Magic Mock String!
print(len(magic_mock))    # 出力: 42
print(magic_mock[5])    # 出力: item value (添え字に関わらず同じ値を返す)
print(int(magic_mock))    # 出力: 99

# 呼び出されたかも記録される
magic_mock.__str__.assert_called_once()
magic_mock.__len__.assert_called_once()
magic_mock.__getitem__.assert_called_once_with(5)
magic_mock.__int__.assert_called_once()

MagicMock を使うことで、コンテナ型 (__len__, __getitem__ など) や数値型 (__int__, __add__ など) を模倣するオブジェクトを簡単に作成できます。

patch を使用する場合も、デフォルトでは MagicMock が使われることが多いですが、new_callable 引数で明示的に Mock や他のクラスを指定することも可能です。

from unittest.mock import patch, Mock, MagicMock
import unittest

class MagicMethodTest(unittest.TestCase):

    # デフォルトでは MagicMock が使われることが多い
    @patch('some_module.some_object')
    def test_default_patch_is_magic(self, mock_obj):
        self.assertTrue(isinstance(mock_obj, MagicMock))
        # マジックメソッドが利用可能
        str(mock_obj)
        mock_obj.__str__.assert_called_once()

    # new_callable で Mock を指定する
    @patch('some_module.some_other_object', new_callable=Mock)
    def test_patch_with_mock(self, mock_obj):
        self.assertTrue(isinstance(mock_obj, Mock))
        self.assertFalse(isinstance(mock_obj, MagicMock))
        # 通常の Mock なので、デフォルトではマジックメソッドは使えない
        with self.assertRaises(TypeError): # もしくは AttributeError など、メソッドによる
             len(mock_obj)

呼び出しの検証: アサーションメソッド

モックオブジェクトの重要な機能の一つは、それがどのように使われたか(呼び出されたか、どのような引数で呼び出されたか)を記録し、検証できることです。そのための便利なアサーションメソッドが多数用意されています。

メソッド 説明
assert_called() モックが少なくとも1回呼び出されたことを表明します。
assert_called_once() モックがちょうど1回だけ呼び出されたことを表明します。
assert_not_called() モックが一度も呼び出されなかったことを表明します。
assert_called_with(*args, **kwargs) モックが最後に指定された引数で呼び出されたことを表明します。
assert_called_once_with(*args, **kwargs) モックがちょうど1回だけ、指定された引数で呼び出されたことを表明します。
assert_any_call(*args, **kwargs) モックが少なくとも1回、指定された引数で呼び出されたことがあるかを表明します。(最後の呼び出しである必要はない)
assert_has_calls(calls, any_order=False) モックが一連の特定の呼び出し(call オブジェクトのリスト)を記録していることを表明します。any_order=True にすると順序を問いません。

これらのアサーションメソッドは、モックオブジェクト自身、またはモックオブジェクトのメソッド(例: mock_obj.method.assert_called_once_with(...))に対して使用します。

from unittest.mock import Mock, call
import unittest

class AssertionTest(unittest.TestCase):
    def test_assertions(self):
        mock_func = Mock()

        # 呼び出し前
        mock_func.assert_not_called()

        # 1回目の呼び出し
        mock_func(1, 2)
        mock_func.assert_called()
        mock_func.assert_called_once()
        mock_func.assert_called_with(1, 2)
        mock_func.assert_called_once_with(1, 2)
        mock_func.assert_any_call(1, 2)

        # 2回目の呼び出し
        mock_func(3, key='value')

        # 呼び出し回数の確認
        self.assertEqual(mock_func.call_count, 2)
        with self.assertRaises(AssertionError): # assert_called_once は失敗する
            mock_func.assert_called_once()

        # 最後の呼び出しの確認
        mock_func.assert_called_with(3, key='value')
        with self.assertRaises(AssertionError): # assert_called_once_with は失敗する
             mock_func.assert_called_once_with(3, key='value')

        # いずれかの呼び出しの確認
        mock_func.assert_any_call(1, 2)
        mock_func.assert_any_call(3, key='value')

        # 呼び出し履歴の確認 (call_args, call_args_list)
        print(f"Last call args: {mock_func.call_args}")
        # 出力例: Last call args: call(3, key='value')
        print(f"All call args list: {mock_func.call_args_list}")
        # 出力例: All call args list: [call(1, 2), call(3, key='value')]

        # assert_has_calls の使用例
        expected_calls = [call(1, 2), call(3, key='value')]
        mock_func.assert_has_calls(expected_calls)

        # 順序が違うと失敗する
        wrong_order_calls = [call(3, key='value'), call(1, 2)]
        with self.assertRaises(AssertionError):
            mock_func.assert_has_calls(wrong_order_calls)

        # any_order=True なら成功する
        mock_func.assert_has_calls(wrong_order_calls, any_order=True)

        # 一部の呼び出しだけでも any_order=True なら成功する
        partial_calls = [call(3, key='value')]
        mock_func.assert_has_calls(partial_calls, any_order=True)
        # any_order=False の場合、連続した呼び出しシーケンスでないと失敗する可能性がある
        with self.assertRaises(AssertionError):
             mock_func.assert_has_calls(partial_calls, any_order=False) # 最後の呼び出しではないため

呼び出し引数を記録するために、unittest.mock.call オブジェクトが内部的に使用されています。assert_has_calls などで期待される呼び出しリストを指定する際に、この call オブジェクトを使います。

また、mock_calls 属性を使うと、メソッドや属性へのアクセスも含めたすべての呼び出し履歴(call オブジェクトのリスト)を取得できます。これは call_args_list よりも詳細な情報を提供します。

from unittest.mock import MagicMock, call

m = MagicMock()
m.foo(1)
m.bar.baz(2, x=3)
m.foo(4)

print(m.method_calls) # メソッド呼び出しのみ (サブモックは含まない)
# 出力: [call.foo(1), call.foo(4)]

print(m.mock_calls) # 属性アクセスやサブモックの呼び出しも含む
# 出力:
# [call.foo(1),
#  call.bar(),
#  call.bar.baz(2, x=3),
#  call.foo(4)]

その他の便利な機能たち

テストコード内で、他と区別可能な特別な値(シングルトン)を使いたい場合があります。例えば、特定のフラグやデフォルト値として、None や他の組み込み定数とは明確に区別したい一意なオブジェクトが必要な時です。

unittest.mock.sentinel は、このような目的のために、アクセスされるたびに一意のオブジェクトを生成します。

from unittest.mock import sentinel, Mock
import unittest

# sentinel オブジェクトに属性名でアクセスすると、その名前を持つ一意のオブジェクトが生成される
DEFAULT_VALUE = sentinel.DEFAULT
ERROR_FLAG = sentinel.ERROR
MISSING = sentinel.MISSING

print(DEFAULT_VALUE)    # 出力例: sentinel.DEFAULT
print(ERROR_FLAG)       # 出力例: sentinel.ERROR
print(DEFAULT_VALUE is sentinel.DEFAULT) # True (同じ名前なら同じオブジェクト)
print(DEFAULT_VALUE is ERROR_FLAG)       # False (名前が違えば違うオブジェクト)
print(DEFAULT_VALUE == sentinel.DEFAULT) # True
print(DEFAULT_VALUE == 'sentinel.DEFAULT') # False (文字列とは異なる)

def process_data(data=sentinel.DEFAULT):
    if data is sentinel.DEFAULT:
        print("Processing with default value.")
    elif data is sentinel.ERROR:
        print("Error flag detected.")
        raise ValueError("Error flag received")
    else:
        print(f"Processing data: {data}")

class SentinelTest(unittest.TestCase):
    def test_sentinel_usage(self):
        mock_processor = Mock(spec=process_data) # autospec を使って引数を検証

        # sentinel を引数として渡す
        mock_processor(sentinel.DEFAULT)
        mock_processor.assert_called_once_with(sentinel.DEFAULT)

        mock_processor_error = Mock(spec=process_data)
        mock_processor_error(sentinel.ERROR)
        mock_processor_error.assert_called_once_with(sentinel.ERROR)

        # sentinel は None や他の値と区別される
        self.assertNotEqual(sentinel.DEFAULT, None)
        self.assertNotEqual(sentinel.DEFAULT, 0)
        self.assertNotEqual(sentinel.DEFAULT, False)

sentinel を使うことで、コードの意図が明確になり、None などが持つ特別な意味合いとの衝突を避けることができます。

クラスのプロパティ(@property デコレータで定義されたもの)をモック化したい場合、通常のメソッドとは少し扱いが異なります。PropertyMock を使うと、プロパティのゲッター、セッター、デリーターの呼び出しを模倣し、検証することができます。

from unittest.mock import patch, PropertyMock
import unittest

class MyClass:
    def __init__(self):
        self._value = 0

    @property
    def value(self):
        print("Getting value")
        return self._value

    @value.setter
    def value(self, new_value):
        print(f"Setting value to {new_value}")
        self._value = new_value

    @value.deleter
    def value(self):
        print("Deleting value")
        del self._value

class PropertyMockTest(unittest.TestCase):
    def test_mock_property(self):
        # MyClass の 'value' プロパティを PropertyMock で置き換える
        # patch の new_callable 引数に PropertyMock を指定する
        with patch('__main__.MyClass.value', new_callable=PropertyMock) as mock_prop:
            instance = MyClass()

            # PropertyMock はデフォルトで Mock オブジェクトを返す
            print(instance.value)
            # 出力例: <PropertyMock name='value' id='...'>

            # return_value を設定してゲッターの動作を模倣
            mock_prop.return_value = 999
            self.assertEqual(instance.value, 999)
            # ゲッターが呼び出されたか確認
            mock_prop.assert_called_once() # プロパティアクセスが呼び出しとして記録される

            # セッターの呼び出しを模倣
            instance.value = 123
            # セッター呼び出しは call(123) として記録される
            mock_prop.assert_called_with(123) # 注意: assert_called_with は最後の呼び出しをチェック

            self.assertEqual(mock_prop.call_count, 2) # ゲッターとセッターで計2回

            # デリーターの呼び出しを模倣
            del instance.value
            # デリーター呼び出しは call() として記録される (引数なし)
            # 最後の呼び出しが del なので assert_called_with() で確認できる
            mock_prop.assert_called_with()

            print(mock_prop.mock_calls)
            # 出力例: [call(), call(123), call()] (ゲッター、セッター、デリーター)

    def test_mock_property_with_side_effect(self):
        # side_effect を使って getter/setter/deleter の挙動を細かく制御
        prop_mock = PropertyMock()
        prop_mock.side_effect = [10, ValueError("Cannot set"), None] # get, set, delete の順

        with patch('__main__.MyClass.value', new_callable=lambda: prop_mock): # new_callable にインスタンスを渡す場合は lambda などを使う
             instance = MyClass()
             self.assertEqual(instance.value, 10) # 1回目の side_effect
             with self.assertRaises(ValueError):
                 instance.value = 20           # 2回目の side_effect (例外)
             del instance.value                # 3回目の side_effect

patchPropertyMock を組み合わせることで、プロパティへのアクセス(取得、設定、削除)をテストで正確にシミュレートし、検証することが可能になります。

unittest.mock とテスティングフレームワーク

unittest.mock は Python の標準ライブラリであり、組み込みの unittest フレームワークと非常に親和性が高いです。これまで見てきた例の多くは unittest.TestCase の中で使用されていました。

一方で、pytest のような他の人気のあるテスティングフレームワークでも unittest.mock は広く利用されています。pytest ユーザー向けには、pytest-mock というプラグインがあり、unittest.mock の機能を pytest のフィクスチャとしてより簡単に利用できるようにしています。

# pytest と pytest-mock を使った例 (別途インストールが必要: pip install pytest pytest-mock)
import os

def check_file_existence_pytest(filepath):
    if os.path.exists(filepath):
        return f"File '{filepath}' exists."
    else:
        return f"File '{filepath}' does not exist."

# pytest のテスト関数では mocker フィクスチャが利用可能になる (pytest-mock により提供)
def test_file_exists_pytest(mocker): # mocker フィクスチャを引数で受け取る
    # mocker.patch を使う (unittest.mock.patch と同様の機能)
    mock_exists = mocker.patch('os.path.exists')
    mock_exists.return_value = True

    result = check_file_existence_pytest("/some/file")
    assert result == "File '/some/file' exists."
    mock_exists.assert_called_once_with("/some/file")

def test_file_does_not_exist_pytest(mocker):
    mock_exists = mocker.patch('os.path.exists', return_value=False) # patch時にreturn_valueなども指定可能

    result = check_file_existence_pytest("/another/file")
    assert result == "File '/another/file' does not exist."
    mock_exists.assert_called_once_with("/another/file")

# mocker.spy を使うと、実際の処理を呼び出しつつ、呼び出しを記録できる
def test_spy_example(mocker):
    real_object = SomeService()
    spy_method = mocker.spy(real_object, 'get_user_data')

    user_data = real_object.get_user_data(55) # 実際のメソッドが呼ばれる

    assert user_data['id'] == 55
    assert user_data['name'] == 'Real User' # 実際の戻り値
    spy_method.assert_called_once_with(55)   # 呼び出しは記録されている

# mocker.stub を使ってダミーオブジェクトを作ることもできる
def test_stub_example(mocker):
    stub = mocker.stub(name='MyStub')
    stub.some_method = mocker.Mock(return_value='stubbed')

    assert stub.some_method() == 'stubbed'
    stub.some_method.assert_called_once()

pytest-mockmocker フィクスチャを提供し、これを通じて patch, spy, stub などの機能を利用できます。unittest.mock の知識は pytest 環境でも直接役立ちますし、pytest-mock を使うことでより pytest らしい書き方でモックを利用できます。

まとめ

unittest.mock は、Python で効果的なユニットテストを書くための強力な武器です 。依存関係を切り離し、テストを高速化・安定化させ、再現困難な状況をシミュレートすることができます。

この記事では、以下の主要な機能について解説しました。

  • MockMagicMock による基本的なモックオブジェクトの作成と操作
  • return_valueside_effect による振る舞いのカスタマイズ
  • patch デコレータとコンテキストマネージャによる依存関係の差し替え
  • autospec=True を使った安全なモック化
  • 各種 assert_* メソッドによる呼び出しの検証
  • sentinel による一意なオブジェクトの利用
  • PropertyMock によるプロパティのモック化
  • pytest-mock など、他のテスティングフレームワークとの連携

モックは非常に便利ですが、使いすぎるとテストが実装の詳細に依存しすぎてしまい、リファクタリングが困難になることもあります。モックは適切に、そして効果的に利用することが重要です。

ぜひ unittest.mock を活用して、あなたの Python プロジェクトのテストをより堅牢で効率的なものにしてください! Happy testing!