Pythonのテストで使うMockとpatchについて
mock
詳しいことは、unittest.mock --- 入門 — Python 3.8.2 ドキュメントにて。
mockはテスト中で呼び出す必要はあるが、本来の動作はさせないで、呼び出しの記録(検証)などは行いたいときに用いるもの。
関数、オブジェクト、クラスなどなんにでも置き換えることができる。
requests.getなどをmockで置き換えることはままあることだと思う。
今回はその利用例をメモとして残しておきたいのでまとめる。ちなみにMockでなくMagicMockを使うのが普通。
また、基本的にpatchデコレータを使っている。
簡単な使い方
requests.getを置き換える例。
some_module内のsome_functionのなかで、requests.getを利用している。これは割と一般的だと思う。
また、import requestsで行っていれば、some_module外のrequests.getを行うことができる。
詳しくは、unittest.mock --- モックオブジェクトライブラリ — Python 3.8.2 ドキュメントのwhere to patchを参照のこと。
import some_module
from unittest.mock import patch, MagicMock
@patch('requests.get')
def test_some_function(mock_func): #1
mc = MagicMock()
mock_func.return_value = mc #2
with open('some_fixture_file', 'r') as f:
mc.text = f.read() #3
t = some_module.some_function()
mock_func.assert_called_once() #4
assert t == 'expected value'
mockの柔らかさはすごい。粘土のようだ。以下はやっていることのメモ。
テスト用関数の引数mock_funcは、@patchによって、渡されるmock。(#1)
requests.getの戻り値は、responseオブジェクトである。モックmcをrequests.getの戻り値として設定する。(#2)
some_functionでは、このうち、textプロパティを利用しているので、
そこにテスト用のテキストファイルの中身を仕掛ける。(#3)
普通にsome_functionをコールして、内部で、モックした関数requests.getが呼び出されていることを確認する。(#4)
このような感じで使える。呼び出されたときの引数もチェックできるし、戻り値もside_effectなどをいじることで動的に変更もできる。
patchは多重も可能
patchは多重にできる。内側が引数の手前にくる。
@patch('a.b')
@patch('requests.post')
@patch('some_module.some_function')
def test_post_today_n225(mf_some_func, mf_post, mf_b):
# ...
非常に便利だが、あまり時間はかけすぎず、シンプルに作っていくべきだ。
また、テストが十分にできていない関数などのpatchは思わぬバグを引き起こす可能性があるので、patchする関数は信頼性のあるものに限ったほうがいいだろう。
以上です。


