2011-11-28

mock.patch


mock ライブラリの patch 関数の挙動を書きます。

パッチャーの start()/stop() メソッドを使う

標準ライブラリの random.random 関数を例にとります。

>>> import random
>>> random.random()
0.90675850364670885
>>> random.random()
0.9838226858480108


patch 関数を実行してみましょう。

>>> from mock import patch
>>> p = patch('random.random')
>>> random.random()
0.27552766919082217

とくに、何も変わったことは起こりません。patch 関数の戻り値は _patch オブジェクトです。ドキュメントにはパッチャーと書いてあります。パッチャーの start() メソッドを呼ぶと変化が起こります。

>>> m = p.start()
>>> m
<mock.Mock object at 0x366f30>
>>> random.random
<mock.Mock object at 0x366f30>

random.random という名前の参照先が、元の乱数生成関数ではなく、Mock オブジェクトに置き換わっています。random.random と p.start() の戻り値は同一の Mock オブジェクトです。

random.random は Mock オブジェクトなので戻り値を書き換えることもできます。

>>> random.random() >>> m.return_value = 100 >>> random.random() 100 >>> random.random.return_value = 0 >>> m() 0 >>> random.random() 0

パッチャーには stop() メソッドがあり、これを実行すると元に戻ります。

>>> p.stop()
>>> random.random
<built-in method random of Random object at 0x6b3b6210>
>>> random.random()
0.029689653478273681

テストで使う

def foo(x):
    return random.random() * x


関数 foo をテストする場合を考えます。テストすべきは、 

  1. random.random を引数なしで 1 度呼び出したこと。
  2. random.random() の戻り値に、x をかけたものが返ること。
です。random 関数の戻り値がわかっていれば、テストできますね。モックしましょう。

import random
import unittest
import mock

def foo(x):
    return random.random() * x

class MyTestCase(unittest.TestCase):
    def test(self):
        # random.random が常に1を返すようモックる
        p = mock.patch('random.random')
        p.start()
        random.random.return_value = 1
        # テスト対象関数を呼び出す
        result = foo(2)
        # random.random() を1度呼んでいることを確認
        self.assertEqual(random.random.call_count, 1)
        self.assertEqual(random.random.call_args, ((), {}))
        # 戻り値をテスト
        self.assertEqual(result, 2)
        # モックを戻す
        p.stop()  

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


これでテストできるようになりました。ですが、問題がありまして、テスト中に例外が出ると p.stop() が呼ばれません。random.random はモックのままなので、他のテストが実行されたきに、意図しない結果になることもあります。

というわけで、必ず実行されるように、setUp と tearDown で、パッチャーの start() と stop() 使います。

class MyTestCase(unittest.TestCase):
    def setUp(self):
        # random.random が常に1を返すようモックる
        self.p = mock.patch('random.random')
        self.p.start()
        random.random.return_value = 1

    def tearDown(self):
        # モックを戻す
        self.p.stop()

    def test(self):
        # テスト対象関数を呼び出す
        result = foo(2)
        # random.random() を1度呼んでいることを確認
        self.assertEqual(random.random.call_count, 1)
        self.assertEqual(random.random.call_args, ((), {}))
        # 戻り値をテスト
        self.assertEqual(result, 2)

つづき

patch をデコレータや、コンテクストマネージャとして使えるのですが、それは、また今度。