Python のテスト書いているときに関数内におけるインスタンスメソッドの呼び出しをテストをしたくなったらハマったので忘れないうちにメモ。pytest を使っているのでサンプルもそれ前提で書いている。
例えばこんなコードがあるとする ( target.py とする)
import use_library
def target_method(a, b):
instanse = use_library.SampleClass(a)
instanse.sample_method(b)
target_method
は特に何も返さないのだけど、
内部でちゃんと instanse.sample_method
が想定されるパラメータを受けて呼び出されるのかをテストしたい状況があった。
メソッド等がどうやって呼び出されたのかをテストするには Python だと mock を使えば簡単にできる。 ただ今回みたいに関数の内部で生成されたインスタンスのメソッドの場合どうやってモックすればいいのか結構悩んだ。
最初は以下のような感じで mock しようとした。
excepted
はこのテストケースで instanse.sample_method
の引数に渡っていて欲しい文字列。
from unittest import mock
def test_target_method():
my_mock = mock.Mock()
excepted = 'B'
with mock.patch('target.use_library', my_mock):
import target
target.target_method(a='A', b='B')
my_mock.SampleClas().sample_method.assert_called_with(excepted)
このとき my_mock
は target.target_method
内部における use_library.SampleClass
(use_library
が my_mock
に差し替わってる) の呼び出しまでは記録してる。ただ use_library.SampleClass
で初期化されたインスタンスである instanse
に関するところまでは追跡できない。
なので my_mock.SampleClas().sample_method.assert_called_with('B')
とかやっても「my_mock.SampleClas().sample_method
は呼び出されてない」ということになってテストが失敗する。
可能であれば use_library
が my_mock
に差し替わっている状況の with 構文のなかで instanse.sample_method.assert_called_with('B')
みたいに呼び出せればいいのだけれどそうもいかない。
そんな感じであの手この手で色々試していくなかで、公式ドキュメントの unittest.mock のページ をよく見てみると mock_calls
と assert_has_calls
というものがあることを発見した。
-
mock_calls の説明
mock_calls は、メソッド、特殊メソッド、 そして 戻り値のモックまで、モックオブジェクトに対する すべての 呼び出しを記録します。
-
assert_has_calls の説明
mock_calls は、メソッド、特殊メソッド、 そして 戻り値のモックまで、モックオブジェクトに対する すべての 呼び出しを記録します。
これらをを見て「なんとかなりそう!」と試してみたところ、うまくいった。
assert_has_calls
の引数には mock.call オブジェクト を要素とするリスト型で渡す必要があるのでそこだけ注意。1 先程のテストコードを以下のような感じにするとうまく行く
from unittest import mock
def test_target_method():
my_mock = mock.Mock()
excepted = [mock.call.SampleClass().sample_method('B')]
with mock.patch('target.use_library', my_mock):
import target
target.target_method(a='A', b='B')
my_mock.assert_has_calls(excepted)
ちょっと書き方がややこしいけれど、 無事に関数内部のインスタンスメソッドの呼び出し方をテストできるようになった。
参考リンク
以下はこの記事の内容に直接触れてるわけではないけれど、Mock の基本的な使い方が非常によくまとまっているので貼っておく。
-
print(my_mock.mock_calls)
で実際に表示されているものに合わせて書いていくのが手っ取り早いと思う。 ↩