はじめに: なぜモックが必要なのか?
ソフトウェア開発において、テストは品質を担保するために不可欠なプロセスです。しかし、現実世界のシステムは複雑で、多くの外部依存関係(データベース、API、ファイルシステムなど)を持っています。これらの依存関係は、テストの実行を遅くしたり、不安定にしたり、あるいは実行そのものを困難にする場合があります。
例えば、外部APIを呼び出す機能をテストする場合、毎回実際のAPIを呼び出すのは非効率的ですし、ネットワークの問題やAPI側の都合でテストが失敗する可能性もあります。また、特定の条件下でのみ発生するエラーケースを再現するのも難しいでしょう。
ここで活躍するのがモック (Mock) です。モックとは、テスト対象のコードが依存しているオブジェクトや関数を、本物の代わりに模倣する「偽物」のオブジェクトです。モックを使うことで、以下のようなメリットがあります。
- テストの分離: テスト対象のコードを、その依存関係から切り離し、ユニットテストを純粋なものに保てます。
- テストの高速化: 時間のかかる処理(ネットワーク通信、DBアクセスなど)を模倣することで、テストの実行時間を大幅に短縮できます。
- テストの安定化: 外部要因によるテストの失敗を防ぎ、安定したテスト結果を得られます。
- エッジケースのテスト: 例外発生や特定の値の返却など、再現が難しい状況を簡単にシミュレートできます。
Pythonでは、標準ライブラリの unittest.mock
モジュールが強力なモック機能を提供しています。(Python 3.3より前のバージョンでは、mock
というサードパーティライブラリとして提供されていましたが、Python 3.3以降で標準ライブラリに取り込まれました。)
このブログでは、unittest.mock
の基本的な使い方から、より高度なテクニックまで、具体的なコード例を交えながら詳しく解説していきます。これを読めば、あなたもモックを使いこなし、より効率的で信頼性の高いテストを書けるようになるはずです!
基本のキ: Mockオブジェクトの作成と操作
unittest.mock
の中心となるのが Mock
クラスです。まずは、この Mock
オブジェクトを作成し、基本的な操作を見ていきましょう。
驚くべきことに、Mock
オブジェクトは、存在しない属性にアクセスしたり、存在しないメソッドを呼び出したりしても、エラーを発生させません。代わりに、アクセスされた属性名やメソッド名を持つ新しい Mock
オブジェクトを返します。これは非常に柔軟ですが、タイプミスなどに気づきにくいという側面もあります(後述する spec
や autospec
で対処できます)。
戻り値と属性の設定
モックオブジェクトのメソッドが特定の値を返すように設定したり、属性に値を設定したりすることができます。
return_value
属性を使うことで、メソッド呼び出し時の戻り値を指定できます。また、通常の属性と同じように値を代入することで、モックオブジェクトの属性を設定できます。
副作用のシミュレーション: side_effect
メソッドが呼び出されるたびに異なる値を返したり、例外を発生させたりするなど、より複雑な振る舞いをさせたい場合は side_effect
属性を使用します。
side_effect
は非常に強力で、以下のような設定が可能です。
- イテラブル (リスト、タプルなど): 呼び出されるたびにイテラブルの次の要素を返します。要素がなくなると
StopIteration
が発生します。 - 例外クラスまたはインスタンス: 呼び出されると指定された例外を発生させます。
- 関数 (呼び出し可能オブジェクト): 呼び出されると、その関数が実行され、その関数の戻り値がモックの戻り値となります。関数にはモックが受け取った引数がそのまま渡されます。
None
:side_effect
の設定を解除します。
依存関係の差し替え: patch デコレータとコンテキストマネージャ
実際のテストでは、特定のモジュール内の関数やクラス、オブジェクトなどを一時的にモックに差し替えたい場合がほとんどです。unittest.mock.patch
は、これを簡単に行うための強力なツールです。デコレータとしても、コンテキストマネージャとしても使用できます。
patch デコレータの使い方
テスト関数やテストクラスのメソッドにデコレータとして適用します。デコレータで指定した対象が、関数の実行中だけモックオブジェクトに置き換えられます。モックオブジェクトは、テスト関数の引数として渡されます。
例として、os.path.exists
をモック化してみましょう。
@patch('os.path.exists')
のように、モック化したい対象を文字列で指定します。この文字列は、対象がインポートされる際のパス(どこで使われているか)を指定する必要があります。例えば、my_module.py
の中で import os
して os.path.exists
を使っている場合、パッチする対象は 'my_module.os.path.exists'
になります。これは少し混乱しやすい点なので注意が必要です。通常は、テスト対象のモジュールから見たインポートパスを指定します。
複数のデコレータを使用する場合、引数は下から上への順序で渡されます。
patch コンテキストマネージャの使い方
with
文を使って、特定のブロック内だけでモック化することもできます。
コンテキストマネージャは、テスト関数全体ではなく、一部の処理だけをモック化したい場合に便利です。
patch.object
特定のオブジェクトの属性やメソッドだけをモック化したい場合は、patch.object
を使うと便利です。対象オブジェクトと、モック化したい属性名を指定します。
より安全なモック: autospec=True
通常の Mock
や patch
では、存在しない属性やメソッドにアクセスしたり、間違った引数で呼び出したりしてもエラーになりません。これは便利ですが、テスト対象のインターフェース(API)の変更に気づきにくくなるという欠点があります。
ここで役立つのが autospec=True
オプションです。patch
や Mock
の作成時にこのオプションを指定すると、モックは元のオブジェクトの仕様(属性、メソッド、引数シグネチャ)を引き継ぎます。これにより、元のオブジェクトに存在しない属性やメソッドにアクセスしようとしたり、間違った引数でメソッドを呼び出そうとしたりすると、エラーが発生するようになります。
autospec=True
(または spec=ClassName
や spec_set=ClassName
) を使うことで、より安全でリファクタリングに強いテストを書くことができます。特に理由がない限り、積極的に利用することをお勧めします。
マジックメソッドもお任せ: MagicMock
Pythonには、__str__
, __len__
, __getitem__
のような、ダブルアンダースコアで囲まれた特殊なメソッド(マジックメソッドやダンダーメソッドと呼ばれます)があります。通常の Mock
オブジェクトは、デフォルトではこれらのマジックメソッドを模倣しません。
マジックメソッドも含めてモック化したい場合は、Mock
のサブクラスである MagicMock
を使用します。
MagicMock
を使うことで、コンテナ型 (__len__
, __getitem__
など) や数値型 (__int__
, __add__
など) を模倣するオブジェクトを簡単に作成できます。
patch
を使用する場合も、デフォルトでは MagicMock
が使われることが多いですが、new_callable
引数で明示的に Mock
や他のクラスを指定することも可能です。
呼び出しの検証: アサーションメソッド
モックオブジェクトの重要な機能の一つは、それがどのように使われたか(呼び出されたか、どのような引数で呼び出されたか)を記録し、検証できることです。そのための便利なアサーションメソッドが多数用意されています。
メソッド | 説明 |
---|---|
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(...)
)に対して使用します。
呼び出し引数を記録するために、unittest.mock.call
オブジェクトが内部的に使用されています。assert_has_calls
などで期待される呼び出しリストを指定する際に、この call
オブジェクトを使います。
また、mock_calls
属性を使うと、メソッドや属性へのアクセスも含めたすべての呼び出し履歴(call
オブジェクトのリスト)を取得できます。これは call_args_list
よりも詳細な情報を提供します。
その他の便利な機能たち
sentinel: 一意なオブジェクトの生成
テストコード内で、他と区別可能な特別な値(シングルトン)を使いたい場合があります。例えば、特定のフラグやデフォルト値として、None
や他の組み込み定数とは明確に区別したい一意なオブジェクトが必要な時です。
unittest.mock.sentinel
は、このような目的のために、アクセスされるたびに一意のオブジェクトを生成します。
sentinel
を使うことで、コードの意図が明確になり、None
などが持つ特別な意味合いとの衝突を避けることができます。
PropertyMock: プロパティのモック化
クラスのプロパティ(@property
デコレータで定義されたもの)をモック化したい場合、通常のメソッドとは少し扱いが異なります。PropertyMock
を使うと、プロパティのゲッター、セッター、デリーターの呼び出しを模倣し、検証することができます。
patch
と PropertyMock
を組み合わせることで、プロパティへのアクセス(取得、設定、削除)をテストで正確にシミュレートし、検証することが可能になります。
unittest.mock とテスティングフレームワーク
unittest.mock
は Python の標準ライブラリであり、組み込みの unittest
フレームワークと非常に親和性が高いです。これまで見てきた例の多くは unittest.TestCase
の中で使用されていました。
一方で、pytest
のような他の人気のあるテスティングフレームワークでも unittest.mock
は広く利用されています。pytest
ユーザー向けには、pytest-mock
というプラグインがあり、unittest.mock
の機能を pytest
のフィクスチャとしてより簡単に利用できるようにしています。
pytest-mock
は mocker
フィクスチャを提供し、これを通じて patch
, spy
, stub
などの機能を利用できます。unittest.mock
の知識は pytest
環境でも直接役立ちますし、pytest-mock
を使うことでより pytest
らしい書き方でモックを利用できます。
まとめ
unittest.mock
は、Python で効果的なユニットテストを書くための強力な武器です 。依存関係を切り離し、テストを高速化・安定化させ、再現困難な状況をシミュレートすることができます。
この記事では、以下の主要な機能について解説しました。
Mock
とMagicMock
による基本的なモックオブジェクトの作成と操作return_value
とside_effect
による振る舞いのカスタマイズpatch
デコレータとコンテキストマネージャによる依存関係の差し替えautospec=True
を使った安全なモック化- 各種
assert_*
メソッドによる呼び出しの検証 sentinel
による一意なオブジェクトの利用PropertyMock
によるプロパティのモック化pytest-mock
など、他のテスティングフレームワークとの連携
モックは非常に便利ですが、使いすぎるとテストが実装の詳細に依存しすぎてしまい、リファクタリングが困難になることもあります。モックは適切に、そして効果的に利用することが重要です。
ぜひ unittest.mock
を活用して、あなたの Python プロジェクトのテストをより堅牢で効率的なものにしてください! Happy testing!