今川館

都内勤務の地味OLです

minimockの使い方

pythonのモックとしてminimockというライブラリがある。
日本語のドキュメント・ブログが見当たらないので使い方をここに書いておく。

minimockを構成するモジュール

  1. mock関数 -- モックを当てる。
  2. restore関数 -- 当てたモックを解除する。
  3. Mockクラス -- mockより詳細にモックを当てるために使うオブジェクト。

mock関数とrestore関数でモックを当てる・解除するというのはわかりやすい。
しかし、複雑なケースではMockオブジェクトを使う必要があるのでここで説明する。

あと、minimockを使う上ではまりやすい点を書く。

基本的な使い方

minimockの基本は

  1. モックに差し替えるモジュールをインポートする。
  2. mock関数を呼ぶ。或いはMockインスタンスを代入する。
  3. 使い終わったらrestoreで戻す。

である。
例えば、以下のphilosophyというモジュールのテストを書くとしよう。

[philosophy.py]

# -*- coding:utf-8 -*-
import urllib

def soccer(num):
    url = r'http://en.wikipedia.org/w/index.php?title=Monty_Python%%27s_Flying_Circus&limit=%s&action=history'
    archimedes = urllib.urlopen(url % num)
    print '!!! eureka !!!', archimedes and archimedes.headers.dict['content-length']
    return 2 * num

philosophyはurllibモジュールを使ってURLの問い合わせを行ってしまうので、その部分をモックに差し替えたい。
そういう場合、以下の通りやるとURLの問い合わせを回避できる。

[test_philosophy.py]

# -*- coding:utf-8 -*-
import unittest
from minimock import mock, Mock, restore
import philosophy

class PhilosophyTest(unittest.TestCase):
    def setUp(self):
        import urllib
        mock('urllib.urlopen')

    def tearDown(self):
        restore()

    def test(self):
        self.assertEquals(philosophy.soccer(1), 2)

if __name__ == '__main__':
    unittest.main()

使用上の注意 -- 対象のオブジェクトを狙ってモックを適用しよう

minimockを使うとき、注意しなければいけないことが一つある。
それは、モックに差し替えるオブジェクトをちゃんと狙い撃ちしようねってことだ。
例えば、philosophyと同じようなspanishというモジュールのテストを書こうとする。

[spanish.py]

# -*- coding:utf-8 -*-
from urllib import urlopen

def inquisition(num):
    url = r'http://en.wikipedia.org/w/index.php?title=The_Spanish_Inquisition_(Monty_Python)&limit=%s&action=history'
    cardinal_fang = urlopen(url % num)
    print '=== the result ===', cardinal_fang and cardinal_fang.headers.dict['content-length']
    return 2 * num

spanishのトップレベルでurllibではなく、urlopenをfromインポートしている所に注意だ。
philosophyの要領で今度も

class SpanishInquisitionTest(unittest.TestCase):
    def setUp(self):
        import urllib
        mock('urllib.urlopen')

こんな風に書いてしまうとモックにならず、URLの問い合わせが実際に行われてしまう。
fromインポートするとオブジェクトがコピーされてしまうので、この場合はurllib.urlopenではなくてspanish.urlopenを狙い撃ちしなければならないのだ。

以下の通りにするとちゃんとurlopenの処理を潰せる。
(mock関数だとうまくいかなかったので、Mockインスタンスを代入して対処)

[test_spanish.py]

# -*- coding:utf-8 -*-
import unittest
from minimock import mock, Mock, restore
import spanish

class SpanishInquisitionTest(unittest.TestCase):
    def setUp(self):
        spanish.urlopen = Mock('spanish.urlopen')
        mock('spanish.urlopen')
    
    def tearDown(self):
        restore()

    def test(self):
        self.assertEquals(spanish.inquisition(7), 14)

if __name__ == '__main__':
    unittest.main()

モックにダミーの値を返戻させたいとき -- returnsを使う

mock関数の呼び出しにreturnsという引数を取れるのでこれを使う。

例えば、urllib2.bulid_openerが返したopenerを使って問い合わせを行う場合、Mockオブジェクトを使って以下のようにできる。

[alength.py]

# -*- coding:utf-8 -*-
import urllib2
def get_length():
    handler = urllib2.HTTPBasicAuthHandler()
    handler.add_password('Title', 'akakakakakakak.com', 'akst', 'heatup')
    opener = urllib2.build_opener(handler)
    res = opener.open('http://akakakakakakak.com?heatup=1')
    return len(res.read()) #=> 'f' * 555

[test_a.py]

from minimock import mock, Mock, restore
import unittest
import alength
from StringIO import StringIO
class ALengthTest(unittest.TestCase):
    def setUp(self):
        alength.urllib2.build_opener = Mock('build_opener', \
            returns=Mock('opener',\
                open=Mock('opener.open', returns=StringIO('f'*555))))

    def tearDown(self):
        restore()

    def test(self):
        self.assertEquals(alength.get_length(), 555)

if __name__ == '__main__':
    unittest.main()

Mockのコンストラクタには任意の名前の属性を指定できる。
今回はopenerのモックのopenに更にMockを返すよう定義した。

状況によってモックが返す値を変えたいとき -- returns_funcの使い方

Mockのコンストラクタにはreturns_funcという引数を渡せる。
これを使うと、callableな属性をcallableなモックで置き換えられる。

例えば、さっきのbuild_openerの例でopener.openを3回呼び出して、それぞれ返す値を変えてテストしたい場合などに使うと良い。

def setUp(self):
    files = [ StringIO(x) for x in ['f' * 555, 'm' * 432, 'h' * 156]]
    alength.urllib2.build_opener = Mock('build_opener',
                                    returns=Mock('opener', open=Mock('opener.open',
                                        returns_func=lambda *args, **kwargs: files.pop(0))))

とにかく呼べればいいので引数が*args, **kwargsのlambdaにした。
上記コードにすると、build_openerが返すオブジェクトのopenがfiles.pop(0)を包んでいるlambdaに置き換わる。
つまり、openを呼ぶとfilesのリストから先頭要素を取り出して返すようになるので、呼ぶたびに戻り値が変わる。