このBlogは移転しました。今後は aish.dev を御覧ください。

mockはこう使え

最近、Mockライブラリ http://www.voidspace.org.uk/python/mock/ を使ってみたのでメモ。

このライブラリは、その性質上、動的にメソッドや属性を作成するケースが多く、普通のPythonライブラリのようにイントロスペクションに頼って使い方を調べるのは難しい。本気で使うならまじめにドキュメントを読み込む必要がある。

関数の置き換え

テスト中に呼び出される関数をMockで置き換える例。ここでは、関数 myapp.utils.func1() を置き換える。

from mock import Mock

import myapp.utils
# myapp.utils.func1 を、常に100を返す関数に置き換える
myapp.utils.func1 = Mock(return_value=100)  

戻り値が定数でない場合は、Mock()side_effectで戻り値を作成する関数を指定する。

def ret_value(a, b,c):
    return a+b+c

import myapp.utils
myapp.utils.func1 = Mock(side_effect=ret_value) 

myapp.utils.func1(100, 200, 300) # ret_value(100, 200, 300) が呼び出される

side_effectに例外型を指定すると、呼び出し時に例外が発生する。

import myapp.utils
myapp.utils.func1 = Mock(side_effect=ValueError) 

myapp.utils.func1(100, 200, 300) # ValueErrorが発生する

クラスの置き換え

class Spam:
    def __init__(self, ham):
        pass
    def egg(self):
        return 100

こんなクラスを置き換えるばあい、次のように書ける。

import myapp.utils
myapp.utils.Spam = Mock()
myapp.utils.Spam.return_value.egg.return_value = 100

こうも書ける。

myapp.utils.Spam = Mock(**{
    'return_value.egg.return_value':100,
})

動的な置き換え

mock.patch()を使って、一時的に置き換えて自動的に元に元通りに復元することが出来る。

patch()をデコレータとして使用すれば、関数の実行中のみ置き換えられる。

from mock import patch

def func():
    return myapp.utils.spam()

@patch('myapp.utils.spam', return_value=100)
def testfunc():
    ret = func()
    assert ret == 100

また、コンテキストマネージャとしても使える。

def func():
    return myapp.utils.spam()

def testfunc():
    with patch('myapp.utils.spam', return_value=100) as m:
        ret = func()
        assert ret == 100

patch()に渡す引数のうち、先頭の置き換え対象の指定以外は、Mock()に渡される。

def func():
    return myapp.utils.SpamClass().ham()

def testfunc():
    with patch('myapp.utils.SpamClass', **{return_value.ham.return_value:100} as m:
        ret = func()
        assert ret == 100

コンテキストマネージャでpatch()を使う場合、複数のオブジェクトを同時に置き換える時に

def test():
    with patch('testapp.func1') as m1:
        with patch('testapp.func2') as m2:
            with patch('testapp.func3') as m3:
                with patch('testapp.func4') as m4:
                     func()

なんて書くのはみっともない。Python2.xなら、contextlib.nested()を使ってこう書こう。

from contextlib import nested
def test():
    with nested(
        patch('testapp.func1'),
        patch('testapp.func2'),
        patch('testapp.func3'),
        patch('testapp.func4'))
    as (m1, m2, m3, m4):

        func()

呼び出し履歴の確認

Mockオブジェクトの呼び出しは、Mock.call_count等の属性値で確認できる。

from mock import *

def test(*args, **kwargs):
    pass
    
def f(*args, **kwargs):
    return test(*args, **kwargs)
    
def func():
    with patch('__main__.test', return_value=100) as m:
        print f(1,2,3,abc=100)
        print f(4,5,6,abc=200)

        print "call_count:", m.call_count
        print "call_args:", list(m.call_args)
        print "call_args_list:", m.call_args_list

if __name__ == '__main__':
    func()
call_count: 2
call_args: [(4, 5, 6), {'abc': 200}]
call_args_list: [call(1, 2, 3, abc=100), call(4, 5, 6, abc=200)]

call_argscall_args_listは、mockを呼び出したときの引数をcallオブジェクトに保存して格納している。

callオブジェクトから引数を取り出すときには、次のように記述する。

args, kwargs = m.call_args
print "引数:%s キーワード引数:%s" % (args, kwargs)