【Python】これだけ!pytestのモックまとめ!

こんにちは!
システム開発部の福井です!

さて、本ブログの執筆を行っている2025年の干支が「巳」ということで、
今回はPythonのテストフレームワークである pytestのモック についてご紹介いたします。

タイトルには「モックまとめ」とありますが、あくまで筆者の体験から独断と偏見で選んだ「これだけは押さえておきたい」という機能や使い方に絞っています。

なので、まだpytestを触ったことがない方やこれから使う予定がある方にとって少しでも参考になれば幸いです。

はじめに

【前置き】

前提として、本ブログでは基本的には pytest-mock というpytestのプラグインを使用した場合のコードを記載いたします。
Python標準ライブラリの unittest.mock とは記法が異なりますので、あらかじめご留意ください。
本ブログでは unittest.mock についての解説は行わないため、詳しく知りたいという方は下記Pythonの公式ドキュメントを参照してください。
https://docs.python.org/3/library/unittest.mock.html

【pytestをまだ触った事がない方に向けて】

また、これからいくつかサンプルのテストコードも記載しますが、pytestをまだ触った事がない方は以下内容を踏まえてご覧いただけますと幸いです。

以下はテストメソッドで実装されるモックの一例となりますが、引数にあるmockerについては、pytest-mockで提供されるフィクスチャとなります。
そのため、pytest-mockがインストールされていれば、特段 import等の必要なく使用できます。

def test_get_name(mocker):

    mocker.patch.object(
        (省略)
    )

本ブログでは、「フィクスチャとは何か?」や「mocker とは何か?(mocker の実態について)」の解説は行っておりませんので、ご興味がある方は pytest, pytest-mock の公式ドキュメントを参照してみてください。
https://docs.pytest.org/en/stable/
https://pypi.org/project/pytest-mock/

メソッド・プロパティをモックする

では、早速本編に参ります。
まずはじめに、メソッドやプロパティのモックについて説明いたします。

インスタンスメソッドをモックする

最初は、利用シーンが多いであろうインスタンスメソッドのモックについての紹介です。
例えば、UserInfoというクラスがあり、そこに get_name() というstr型の値を返却するメソッドが定義されている場合、実装例としては以下となります。

def test_get_name(mocker):

    test_name = "テストネーム"

    # UserInfoクラスのget_name()のモック
    mocker.patch.object(
        UserInfo,
        "get_name",
        return_value=test_name
    )

第1引数と第2引数で、対象となるクラスオブジェクト・メソッドを指定して頂き、
戻り値を指定したい場合は return_value の値を指定すれば出来上がりです。

特定のモジュールのメソッドをモックする

クラスで定義されてないメソッドをモックしたい場合は、以下のようにモジュールを指定してモックする事もできます。

def test_get_name(mocker):

	test_name = "テストネーム"

	# app.services.userモジュールのget_name()のモック
	mocker.patch(
		"app.services.user.get_name",
		return_value=test_name
	)

対象のモジュールとモジュール内で実行されるメソッドを文字列で指定してあげる事でモックをします。
実装する場合はモジュールの指定に誤りが無いよう注意してください。

同じメソッドが複数回実行される場合に異なる戻り値を指定してモックする

テスト対象のメソッドが、別の同一メソッドを複数回実行するケースもあるかと思います。
戻り値が固定でも問題ない場合は上記の方法でテスト実装できますが、処理によっては戻り値に応じて処理ルートが変わる場合もあるかと思います。

そんな時に役立つのが、side_effect となります。実装例は以下となります。

def test_get_name(mocker):
 
	test_name_1 = "田中一郎"
	test_name_2 = "鈴木次郎"
 
	# UserInfoクラスのget_name()のモック
	mocker.patch.object(
	    UserInfo,
	    'get_name',
	    side_effect=[
	        test_name_1,   # 1回目に実行された時の戻り値
	        test_name_2,   # 2回目に実行された時の戻り値
	    ]
	)

side_effect は簡単にいうとモック対象の挙動をカスタマイズできる便利なプロパティであり、上記例以外にも関数を指定したりできるのですが、本題から逸れるためこのお話は割愛させて頂きます。
本題に戻りまして、実行回数毎に異なる値を指定したい場合は上記のように返却したい値の順番にリストで指定すれば設定する事ができます。
side_effect についてはこの後も別の使い方でご紹介することもあり、ほかにも様々な使い方ができる便利な機能となっています。

プロパティをモックする

メソッドだけではなく、プロパティをモックしたい場合は、PropertyMock を使用する事でモックができます。
例えば、UserInfoクラスに名前を格納するfirst_nameというプロパティがあった場合、実装例は以下となります。

def test_first_name(mocker):

	first_name_taro = "太郎"

	# UserInfoの first_nameプロパティをモック
	mocker.patch.object(
		UserInfo,
		"first_name",
		new_callable=PropertyMock,
		return_value=first_name_taro
	)

PropertyMock自体は Python標準ライブラリである unittest.mock で提供されるクラスで、今回のように特定のクラスのプロパティをモックするために使用します。(通常のモックで使用されるMockクラスとは別のものとなります。)
コード自体はメソッドをモックする場合とそこまで大きな違いはありませんが、new_callable=PropertyMock のように new_callableの指定が必要である点に注意してください。

モックの実行検証をする

ここまではメソッドやプロパティのモックの方法についてご紹介しましたが、
実際に単体テストなどを実装する時は、モックした処理が想定通り呼ばれているかどうか を検証したい場面があるかと思います。
ここからは、そんな「モックしたメソッドの実行検証 についてご紹介いたします。

実行されたかどうかを検証する

では、最初に「実行されたかどうか」のみを検証する方法についてご紹介します。
シンプルに「1回呼ばれたかどうか」だけを検証したいのであれば、assert_called_once() というメソッドを使用することで実装できます。
実装例は以下となります。

def test_get_name(mocker):
 
	test_name = "テストネーム"
 
	# UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		return_value=test_name
	)


	(※メソッド実行の処理など)


	# get_name()のモックが1回実行されたこと
	mock_get_name.assert_called_once()

また、複数回実行される想定で、呼ばれた回数を検証したいという場合は、
以下のように assertcall_count というプロパティを使用することで実装できます。

def test_get_name(mocker):
 
	test_name = "テストネーム"
 
	# UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		return_value=test_name
	)


	(※メソッド実行の処理など)


	# get_name()のモックが2回実行されたこと
	assert mock_get_name.call_count == 2

 

想定した引数で実行されたかどうかを検証する

次に「モックしたメソッドに渡された引数が想定通りかどうか」を検証する方法についてご紹介します。
こちらは、assert_called_with() というメソッドを使用することで実装できます。
実装例は以下となります。

def test_get_name(mocker):
 
	test_name = "テストネーム"

	# モック対象メソッドに渡される引数の想定値
	expected_user_id = 123
 
	# UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		return_value=test_name
	)


	(※メソッド実行の処理など)


	# get_name()に渡された引数が、想定値 (expected_user_id) であること
	mock_get_name.assert_called_with(user_id=expected_user_id)

「実行されたかどうか」と「引数」の検証を一緒に行う

ここまでは、「実行されたかどうか」と「引数が想定通りかどうか」を別々に検証する方法でご紹介しましたが、これを一緒に検証する方法もあります。
assert_called_once_with() というメソッドを使用すれば、別々にコードを記述することなく検証ができます。
実装例は以下となります。

def test_get_name(mocker):
 
	test_name = "テストネーム"

	# モック対象メソッドに渡される引数の想定値
	expected_user_id = 123
 
	# UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		return_value=test_name
	)


	(※メソッド実行の処理など)


	# get_name()に渡された引数が想定値 (expected_user_id)、かつ、1回実行されたこと
	mock_get_name.assert_called_once_with(user_id=expected_user_id)

モック対象のメソッドが1回しか呼ばれない想定であれば、assert_called_with() よりも assert_called_once_with() を使った方が簡潔に書けて良いかと思います。

また、モック対象メソッドが複数回呼ばれる想定でも、assert_has_calls() を使用することで、実行検証と引数の検証を同時に行うことができます。
実装例は以下となります。

def test_get_name(mocker):
  
	test_name_1 = "田中一郎"
	test_name_2 = "鈴木次郎"
	
	# モック対象メソッドに渡される引数の想定値
	expected_user_id_tanaka = 1
	expected_user_id_suzuki = 2

    # UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		'get_name',
		side_effect=[
			test_name_1,   # 1回目に実行された時の戻り値
			test_name_2,   # 2回目に実行された時の戻り値
		]
	)


	(※メソッド実行の処理など)

	# get_name() が想定した引数で、順番に実行されていること
	mock_get_name.assert_has_calls([
		mocker.call(expected_user_id_tanaka),
		mocker.call(expected_user_id_suzuki)
	])

モックしたメソッドに渡された引数を取得する

あとは、上記のような検証メソッドではありませんが、モック対象メソッドに渡された引数を取得することもできます。
モックオブジェクトの call_args.args というプロパティから取得することが可能です。
取得した値の型は tuple で、位置引数として取得することができます。
実装例は以下となります。

def test_get_name(mocker):
 
	test_name = "テストネーム"
 
	# UserInfoクラスのget_name()のモック
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		return_value=test_name
	)


	(※メソッド実行の処理など)


	# モック対象メソッドに渡された引数を取得
	args = mock_get_name.call_args.args

	# 1つ目の引数
	args_1 = args[0]

	# 2つ目の引数
	args_2 = args[1]

引数が取得できれば assert を使って検証することもできますし(敢えてassertを使う必要はないかもしれませんが...)、
取得した引数を使って別の処理を書いたりすることもできるので、個人的には覚えておきたいと思いご紹介しました。
ただ、call_args は「最後に実行された時の引数」しか取得できないため、複数回実行されるメソッドの引数を取得したい場合は call_args_list を使用する点に注意してください。

side_effect を使って例外を発生させる

最後はモックオブジェクトを使って例外を発生させる方法をご紹介します。
今回は、先ほど別のトピックでも出てきました side_effect を使用します。
実装例は以下となります。

def test_get_name(mocker):
 
 	test_error_message = "対象のユーザーが存在しません"

	# UserInfoクラスのget_name()で例外を発生させる
	mock_get_name = mocker.patch.object(
		UserInfo,
		"get_name",
		side_effect=Exception(test_error_message)
	)

こちらも実装自体はシンプルで、side_effect に対して発生させたい例外クラスインスタンスを指定すれば出来上がりとなります。
テスト対象のメソッドが例外発生時に何かしら処理をしているのであれば、この方法を使って例外発生時の処理ルートを通すことでテストのカバレッジを上げていくことができます。

まとめ

いかがだったでしょうか。
本ブログでは利用シーンに応じて使うメソッドや実装方法を分けてご紹介させていただきましたが、どれもコード自体はシンプルで簡単に実装できるものばかりだったかと思います。
また、冒頭でも書きましたが、今回の内容はあくまで筆者が「これは覚えておきたい」という内容のみに絞っているため、逆をいえば、ここで紹介していない機能やメソッドがまだまだ沢山あります。
ご興味がある方はぜひ公式ドキュメントを参照しながら、より良い実装や便利な機能を調べてみてください!
今回はここまでとさせていただきます!ではまた!

この記事がお役に立てば【 いいね 】のご協力をお願いいたします!
6
読み込み中...
6 票, 平均: 1.00 / 16
2,104
X facebook はてなブックマーク pocket

この記事をかいた人

About the author

福井 浩人

2020年6月にビヨンドに入社。システム開発部 (横浜オフィス) にて勤務。
業務ではPHPを中心に、ゲームAPIやWebシステムの開発、Shopifyプライベートアプリの開発を担当。
洋楽を主として音楽全般が好きで趣味でギターを弾いている。好きなTV番組は、「探偵!ナイトスクープ」「出没!アド街ック天国」。