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が変わってる可能性もあるかもしれない.