Python: Mock
unittestでは基本的にassert等を用いてテストする為,動作の度にランダムに返す値が変わったり,外部サービスと連携するようなメソッドはテストが難しい. Pythonでは,Mockライブラリを使用することで,上記のようなケースでも比較的簡単にテストすることができる.
Mockって?
もしテストしたい対象が内部で下記のようなランダムな値を返す関数を利用している場合,単体テストが非常に面倒臭い.
import random
def randfunc():
return random.randint(0, 10)
randfunc()
# => 0から10の値
def somefunc():
if 5 > randfunc():
return True
else:
return False
# assert ? == somefunc()
# 実行の度にTrueかFalseどちらを返すか分からないので,assertでテストできない.
上記のような場合,ダミーなクラスや関数を作って,置き換えることで,テストがしやすくなる. Mockでは,Mockオブジェクトを作り,置き換えたい関数に代入することで使用する. ダミーの関数の戻り値はreturn_valueで指定できる.
from mock import Mock
randfunc = Mock()
randfunc.return_value = 10
randfunc()
# => 10
# Mockで作ったダミーの関数は常に10を返す.
動的な置き換え
上記ではMockで置き換えた関数はそれ以降ずっとダミーの関数のままになってしまう. Mockでは,patchデコレータやwith構文を使ったコンテキストマネージャーが用意されており,テスト中のみダミー関数に置き換えることができる.
# some.py
class SomeClass(object):
# some process
# 外部サービスと連携するメソッド
def service():
# some process
return "hogehoge"
# tests.py
from mock import patch
#testItの実行中のみダミー関数に置き換わる
#置き換えたいメソッド,ダミーの関数の戻り値をpatchに指定する
@patch("some.SomeClass.service", return_value=100)
def testIt():
import some
result = some.SomeClass.service()
assert result == 100
またwith構文を使った場合は下記のようになる.
def testIt():
# withブロックの間のみダミー関数に置き換わる
with patch("some.SomeClass.service", return_value=100) as m:
import some
result = some.SomeClass.service()
assert result == 100
# 以降は元の関数が使用される
...
その他
Mockライブラリは非常に機能が豊富で,上記のような基本的な使い方以外にも異なった値や例外を返すことができるside_effectや mockが呼び出された回数をチェックできるassert_called_once_with()等色々と便利な機能があるので,少しずつ調べて使っていきたい.
蛇足
「Python プロフェッショナル プログラミング」 でもMockが紹介されているが一部誤植っぽいのを見つけたのでメモ
書籍でのmockのpatchを使ったサンプルコードは下記だが
@patch("myviews.SomeService")
def test_it(MockSomeService):
mock_obj = MockSomeService.return_value
# 再度return_valueが呼ばれている
mock_obj.return_value = 10
# patchではSomeServiceを置き換えているのに,MyViewが呼ばれている
from myviews import MyView
result = Myview()
# 書籍では代入になっていたけど,sphinxで通らない事もあり,正しいと思われる比較に修正
assert result == 10
実際には下記が正しいと思われる.
@patch("myviews.SomeService")
def test_it(MockSomeService):
MockSomeService.return_value = 10
from myviews import SomeService
result = SomeService()
assert result == 10
まぁ,myviews.pyでのMyViewが下記なら元のコードでも問題ないけれど,ちょっとそこまでは読み取れなかった*1.
# myviews.py
def SomeService():
#some process
return "hogehoge"
def MyView():
return SomeService
参考
*1:書かれた当時とmockのversionが変わってる可能性もあるかもしれない.