2011-12-04

mock.patch をデコレータ、コンテクストマネージャとして使う

patch はコンテクストマネージャでもあるので、with を使った書き方ができます。with でつくられたブロックの中でだけ、モックが機能するようになります。
>>> from mock import patch
>>> with patch('random.random') as m:
...     import random
...     random.random()  # もうモックになっている。戻り値は Mock オブジェクト
...     m.return_value = 0.5
...     random.random()  # 戻り値は 0.5
... 
<mock.Mock object at 0x423ed0>
0.5
>>> random.random()  # モノホンの random 関数
0.84432022018162511
with ブロックの中に入った時点で、random.random はモックになってしまいます。必須ではありませんが、 as m のように書いて、変数 m でモックへアクセスできます。patch().start() の戻り値と同じです。
with ブロックを抜けるときに、パッチャー の stop() メソッドが実行されたのと同じ状態になり、モックではなくなります。
複数のモックを使いたいときには、with ブロックをネストするわけですが、それはちょっと鬱陶しいですよね。インデントが深くなりすぎるかも、なので。そんな時は 標準ライブラリの contextlib.nested を使います。
>>> from contextlib import nested
>>> from mock import patch
>>> from __future__ import with_statement
>>> with nested(patch('random.random'), patch('random.randint')) as (m, n):
...     m.return_value = 0.5
...     n.return_value = 3  # random.randint のモック
patch は、デコレータとしても使えます。
>>> from mock import patch
>>> @patch('random.random') 
... def func(m):
...     import random
...     print random.random()
...     m.return_value = 0.5
...     print random.random()
... 
>>> func()
<mock.Mock object at 0x429730>
0.5
>>> random.random()
0.94254256426687633
関数を patch でデコレートすると、元の関数にMock オブジェクトが渡されるようになります。上の例では、第1引数 m が random.random に対応するモックです。このモックが使われているスコープ内、つまりこの関数内でモックが有効になっています。関数を抜けると(正確には、関数を抜けて、デコレートしている外側のスコープを抜けると)、モックではなくなります。したがって、 func() を呼び出した後で、random.random() を呼び出すと本来の戻り値になります。
unittest モジュールを使ったテストを書く時には、テスト関数に、デコレータからモックを受け取るように書きます。
class MyTest(TestCase):
 @patch('random.random')
 def test1(self, m):
  import random
  m.return_value = 10.0
  self.assertEqual(random.random(), 10.0)
というわけで mock オブジェクトの機能でした。機能を知ることと、使えることはまた別なので、実際にテストでどうするのがいいか、は、別の機会に。私自身、試行錯誤しております。