[python] まだmockで消耗してるの?mockを理解するための3つのポイント
隣の席の人がテスト強化週間とか抜かしていたので自分もちゃんと理解するためになるべくわかりやすくまとめてみようと思います。
この記事は2015 tech-yuruyuru アドベントカレンダー – connpass 15日目の記事です。
mockとは?
mockは特定のオブジェクトの代理をしてユニットテストを円滑に進めるためのモジュールです。
python3.3からはビルトインに入りましたが、それ未満のバージョンではインストールが必要です。以下のようにインストールしてください。
$ pip install mock
インストールしたmockを使う場合は単に「import mock」とすればよいのですが
ビルトインmockを使う場合は、「from unittest import mock」のようにして使うのが一般的です。
(以降、この記事では無用な混乱を避けるため、mockのimport文を省略します。使い方は概ね同じはずです)
個人的にmockを理解する上で重要なポイントは以下の3点だと考えています。
- 1. (大体)どんな振る舞いも表現できるMockオブジェクト
- 2. 任意の名前空間に自身のMockオブジェクトをねじ込むことができるパッチ機能
- 3. ねじ込んだMockオブジェクトがどのように使われたか(呼び出されたか)を記録するキャプチャ機能
これらが合わさるからこそmockは強力なわけですが、一遍には理解しづらい概念かもしれません。
一つ一つ理解していきましょう。
Mockオブジェクト
Mockオブジェクトを一言で表現するなら(大体)どのようなオブジェクトの代わりにもなれる高機能粘土みたいなオブジェクトです。
具体的にはmock.Mockおよび派生クラス(のインスタンス)です。デフォルトではMockクラスを継承したMagicMockが使われます。
違いについては後述しますが、基本的には上位互換のMagicMockを使っていれば問題ありません。
単体で使うことはあまり多くはありませんが、理解するために触ってみましょう。
>>> m = mock.MagicMock(a=1, b=2) >>> # キーワード引数で指定した要素を作成できる >>> m.a 1 >>> m.b 2 >>> # 未知の属性にアクセスしてもAttributeErrorにならず、直接アクセスできる >>> m.c <MagicMock name='mock.c' id='7696574899032'> >>> m.c <MagicMock name='mock.c' id='7696574899032'> >>> m.c.d <MagicMock name='mock.c.d' id='7696573612832'> >>> m.e.f.g <MagicMock name='mock.e.f.g' id='7696573666584'> >>> m.__a <MagicMock name='mock.__a' id='7696573728248'> >>> # 特殊属性は自動生成できない >>> m.__a__ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 570, in __getattr__ raise AttributeError(name) AttributeError: __a__ |
また、初期化する段階で属性を辞書のキーとして指定できます。
これは@shimizukawa氏から教えてもらいました。
>>> # もちろんこのように書くことはできないけど >>> m = mock.MagicMock(a.b.c.d=1) File "<stdin>", line 1 SyntaxError: keyword can't be an expression >>> # これはOK! >>> m = mock.MagicMock(**{'a.b.c.d': 100}) >>> m.a.b.c.d 100 >>> m.a.b.c <MagicMock name='mock.a.b.c' id='140294877762728'> >>> # 初期化後であればconfigure_mockメソッドを使うこともできます >>> m.configure_mock(**{'e.f.g': 200, 'h.i': 300}) >>> m.e.f.g 200 >>> m.h.i 300 |
呼び出し
Mockオブジェクトはcallableなので、(callableじゃないMockもありますが)関数(メソッド)として呼び出すことができます。
返却値を指定したい場合、return_valueを使います。
>>> # 返却値の指定 >>> m = mock.MagicMock(return_value=3) >>> # なにも指定せずに呼び出し >>> m() 3 >>> # 通常の引数を指定して呼び出しても結果は同じ >>> m(4, 5) 3 >>> # キーワード引数も指定して呼び出しても結果は同じ >>> m(6, spam=7, ham=8, egg=9) 3 >>> # あとから変更することもできる >>> m.return_value = 4 >>> m() 4 |
複雑なふるまいを表現したい
Mockオブジェクトはほかにもside_effectという引数(属性)を持っています。
return_valueが毎回同じ返却値しか返せなかったことに対し、side_effectは異なった返却値を返せたり、例外を起こすことが可能です。
>>> # 毎回返却値を変えたい場合 >>> m = mock.MagicMock(side_effect=[10, 11, 12]) >>> # side_effectはイテレータ形式で保存されている >>> m.side_effect <list_iterator object at 0x6ffff8bd6a0> >>> m() 10 >>> m() 11 >>> m() 12 >>> m() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 896, in __call__ return _mock_self._mock_call(*args, **kwargs) File "/usr/lib/python3.4/unittest/mock.py", line 955, in _mock_call result = next(effect) StopIteration >>> # これも後から変更できる >>> m.side_effect = [4, 5] >>> m() 4 >>> m() 5 >>> # 関数を渡すとそのまま実行される >>> def echo(*args, **kwargs): ... print(args, kwargs) ... return 500 >>> m = mock.MagicMock(side_effect=echo) >>> m(1, 2, a=3, *[4, 5], **{'b': 7, 'c': 8}) ((1, 2, 4, 5), {'a': 3, 'c': 8, 'b': 7}) 500 >>> # 例外を発生させたい >>> m = mock.MagicMock(side_effect=IndexError) >>> m() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 896, in __call__ return _mock_self._mock_call(*args, **kwargs) File "/usr/lib/python3.4/unittest/mock.py", line 952, in _mock_call raise effect IndexError >>> m = mock.MagicMock(side_effect=IndexError(u'インデックスエラー')) >>> m() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 896, in __call__ return _mock_self._mock_call(*args, **kwargs) File "/usr/lib/python3.4/unittest/mock.py", line 952, in _mock_call raise effect IndexError: インデックスエラー >>> m.side_effect IndexError('インデックスエラー',) |
return_valueとside_effectを同時に指定した場合、side_effectが優先されます。
もし、あとからside_effectを無効化する場合はNoneを代入するという手もあります。(そんなことはしないほうがいいんだけど)
キャプチャ機能
少々順番が前後しますが、タイミング的にここがベストな気がするので説明します。
Mockオブジェクトは自身が呼び出されたときの引数などをすべて記録しており、あとから参照することができます。
>>> m = mock.MagicMock(return_value=3) >>> m() 3 >>> m(4, 5) 3 >>> m(6, spam=7, ham=8, egg=9) 3 >>> # 呼び出された回数を記憶している >>> m.call_count 3 >>> # どのように呼び出されたかも覚えている >>> m.call_args_list [call(), call(4, 5), call(6, egg=9, spam=7, ham=8)] >>> # call_args_listの各要素にはcallというlistオブジェクトが格納されている >>> m.call_args_list[0] call() >>> # 0番目に通常引数(*args)が格納されていて >>> m.call_args_list[0][0] () >>> # 1番目にキーワード引数(**kwargs)が格納されている >>> m.call_args_list[0][1] {} >>> m.call_args_list[1] call(4, 5) >>> m.call_args_list[1][0] (4, 5) >>> m.call_args_list[1][1] {} >>> m.call_args_list[2] call(6, egg=9, spam=7, ham=8) >>> m.call_args_list[2][0] (6,) >>> m.call_args_list[2][1] {'egg': 9, 'spam': 7, 'ham': 8} >>> # 直前だけを参照する場合はcall_argsを見る >>> m.call_args call(6, egg=9, ham=8, spam=7) >>> # 一度でも呼ばれている場合はcalled属性がTrue >>> m.called True >>> n = mock.MagicMock() >>> # やっぱり呼ばれていないとFalse >>> n.called False >>> n() <MagicMock name='mock()' id='537935583664'> >>> n.called True >>> # 一度だけ呼び出されたかを確認するときにはassert_called_once_withを使う >>> n.assert_called_once_with() >>> n() <MagicMock name='mock()' id='537935583664'> >>> n.assert_called_once_with() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 802, in assert_called_once_with raise AssertionError(msg) AssertionError: Expected 'mock' to be called once. Called 2 times. >>> # 履歴をリセットする >>> m.reset_mock() >>> m.called False |
他にも呼び出されたときの引数をテストするためのメソッドがいくつか用意されています。
>>> o = mock.MagicMock() >>> o(1, b=2) <MagicMock name='mock()' id='537948960080'> >>> o(3, c=4) <MagicMock name='mock()' id='537948960080'> >>> # 直前に呼び出されたかどうかを確認する >>> o.assert_called_with(1, b=2) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 792, in assert_called_with raise AssertionError(_error_message()) from cause AssertionError: Expected call: mock(1, b=2) Actual call: mock(3, c=4) >>> o.assert_called_with(3, c=4) >>> # 一度でも呼ばれていればOKなことを検査したい場合 >>> o.assert_any_call(1, b=2) >>> o.assert_any_call(1, b=2, c=3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.4/unittest/mock.py", line 854, in assert_any_call ) from cause AssertionError: mock(1, b=2, c=3) call not found |
大抵はユニットテストのテストケースを後述するパッチ機能で書き換え、期待通りに呼び出されているかなどに利用されます。
MagicMockとMockの違いについて
MagicMockの「Magic」はMagicMethodの「Magic」です。
通常のMockではサポートされていない四則演算等もMagicMockなら実現できます。
>>> m = mock.Mock() >>> m + 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'Mock' and 'int' >>> n = mock.MagicMock() >>> n + 1 <MagicMock name='mock.__add__()' id='21494145712'> >>> n + 2 <MagicMock name='mock.__add__()' id='21494145712'> >>> m[1] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Mock' object does not support indexing >>> n[1] <MagicMock name='mock.__getitem__()' id='21479687504'> >>> m['a'] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Mock' object is not subscriptable >>> n['a'] <MagicMock name='mock.__getitem__()' id='21479687504'> >>> for i in m: i ... Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Mock' object is not iterable >>> for i in n: i ... |
当たり前ですが、通常のMockには一つもマジックメソッドが存在しないというわけではありません。
明示的に実装しなければならないマジックメソッドがあらかじめ実装されているのがMagicMockというわけです。
パッチ機能
パッチ機能によってオブジェクトの要素を差し替えるわけですが、パッチは決して魔法ではありません。
使いこなすには多少のモジュールの知識が必要です。
まずはmockを使わずに関数のふるまいを変えてみましょう。
| b.py | __main__ | |||
|---|---|---|---|---|
|
|
上記の例を解説すると、__main__で参照しているosもb.pyで参照しているosも実体が同じ為、変更されれば参照している処理すべてが影響を受けます。
os.path.joinはdummy関数によって書き換えられてしまったため、同一プロセス内では常に「dummy」が返却されます。
mockによるパッチは特別なことを行っているわけではありません。
上記でいうdummyの代わりにMockオブジェクトを差し込み、適用範囲を抜けたら元に戻すことでほかの処理に影響しないようにしてくれています。
パッチ機能は「mock.patch」「mock.patch.object」「mock.patch.dict」の3種類があります。
mock.patch
mock.patchは指定したモジュールパス(文字列)が指すオブジェクトをMockオブジェクトに差し替えます。
デコレータかコンテキストマネージャによって適用することができます。
先程の例をmock.patchを使って表現してみましょう。
>>> import os >>> os.path.join('/a/b/c', 'd/e') '/a/b/c/d/e' >>> with mock.patch('os.path.join', return_value='dummy'): ... os.path.join('/a/b/c', 'd/e') 'dummy' >>> os.path.join('/a/b/c', 'd/e') '/a/b/c/d/e' |
上記のようにした場合os.path.joinがMockオブジェクトに差し替わり、return_valueで「dummy」を指定したため常に「dummy」を返却します。
from importを使ったオブジェクトをパッチする
モジュールの理解があれば特筆するようなことでもないのですが、ハマりポイントなような気がするので一応解説しておきます。
from importによって利用可能になったモジュール変数はもとのモジュールから切り離されるため、もとのモジュールをパッチしても書き換えられません。
例の如くos.path.joinを書き換える例です。
>>> from os.path import join >>> # この時点でjoinオブジェクトはos.pathに属していないため書き換えても動かない >>> with mock.patch('os.path.join', return_value='test') as m: ... join('a/b', 'c/d') ... m is join 'a/b/c/d' False >>> # joinオブジェクトが属するモジュールを指定する必要がある(この場合はコンソールなので__main__) >>> with mock.patch('__main__.join', return_value='test'): ... join('a/b', 'c/d') ... m is join 'test' True >>> # もしくはモジュールパス部分から指定する など >>> import os.path >>> with mock.patch('os.path.join', return_value='test') as m: ... os.path.join('a/b', 'c/d') ... m is os.path.join 'test' True |
mock.patch.object
mock.patchがモジュールパスに該当するオブジェクトを差し替えるのに対し、patch.objectは任意のオブジェクトの属性を差し替えます。
>>> class Test(object): ... a = 100 >>> # Dummy.aを200で書き換える >>> with mock.patch.object(Test, 'a', 200) as dummy: ... print(Test.a) ... print(dummy) 200 200 >>> # 第3引数()を指定しない場合、Mockオブジェクトが渡される。 >>> with mock.patch.object(Test, 'a') as Dummy: ... print(Test, Test.a) ... Test.a = 200 ... print(Test.a) <class '__main__.Test'> <MagicMock name='a' id='7696571732304'> 200 >>> Test.a 100 |
さて、mock.patchではなくmock.patch.objectを使うべきケースはどのようなものがあるのでしょうか。
期待しない属性値も巻き込んでしまう場合
書き換えたい対象は常に関数(メソッド)とは限りません。時には定数(変数)を書き換えたいといったこともあるでしょう。
関数の返却値がreturn_valueやside_effectで制御できるのに対し、属性値(モジュール変数など)は上書きするほかありません。
>>> # mの関数としての実行結果は制御できるが m = mock.MagicMock(return_value=1) >>> # m自身を1と評価することはできない >>> m <MagicMock id='7696577957648'> >>> # mを1と評価させたい場合は1を代入すればよい >>> m = 1 |
これをmock.patch、つまりモジュールパスの指定で解決しようとすると、書き換え対象オブジェクトを直接の子(属性)として持つ親オブジェクトのモジュールパスを指定することになりますね。
親オブジェクトを書き換えると期待しない属性が影響を受ける可能性があります。
| a/__init__.py |
|
|
|---|---|---|
| a/b/__init__.py |
|
|
| a/b/c/__init__.py |
|
|
| a/b/c/d.py |
|
(こんな深くなくてもいいんだけど
a.b.c.d.fという関数は、同一モジュール内の変数、a.b.c.d.eという変数に依存しており、書き換えると結果も変わるというシナリオです。
これの書き換えについて、mock.patchとmock.patch.objectそれぞれのやり方で見てみよう。
>>> from a.b.c import d >>> d.f() 100 >>> d.e = 200 >>> d.f() 200 >>> # a.b.c.d.eをmock.patchで書き換えようとすると >>> # 必然的にa.b.c.d.e変数ではなく、a.b.c.dを書き換えることになる >>> with mock.patch('a.b.c.d') as m: ... m.e = 200 ... import a.b.c.d as d ... # 確かに書き変わるが ... print(d.e) ... # d.fも書き換えられてしまうため200が返却されない ... d.f() 200 <MagicMock name='d.f()' id='140269715651216'> >>> # mock.patch.objectで書き換えようとすると以下のようにする >>> with mock.patch.object(d, 'e', 300): ... # d.fは影響を受けないためd.eが返却できる ... d.f() 300 |
Globalスコープに属さない変数を差し替える場合
mock.patchが差し替えられるのはglobalスコープに属する変数に限られます。
>>> class Test(object): ... a = 1 >>> # 関数内で定義されている場合は対応できない >>> with mock.patch('__main__.test1', a=2): ... print(test1.a) ... print(test2.a) 2 1 >>> def test(): ... test1 = Test() ... test2 = Test() ... with mock.patch(__name__ + '.test1', a=2): ... print test1.a ... print test2.a >>> test() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in test File "/home/righ/Tests/libtest2/local/lib/python2.7/site-packages/mock/mock.py", line 1369, in __enter__ original, local = self.get_original() File "/home/righ/Tests/libtest2/local/lib/python2.7/site-packages/mock/mock.py", line 1343, in get_original "%s does not have the attribute %r" % (target, name) AttributeError: <module '__main__' (built-in)> does not have the attribute 'test1' >>> test1 = Test() >>> test2 = Test() >>> # globalスコープに変数が定義されている場合は以下のようにできる >>> with mock.patch('__main__.test1', a=2): ... print(test1.a) ... print(test2.a) 2 1 |
mock.patch.objectでやる場合は以下のようにします。
>>> class Test(object): ... a = 1 >>> def test(): ... test1 = Test() ... test2 = Test() ... with mock.patch.object(test1, 'a', 2): ... print test1.a ... print test2.a >>> # ローカルスコープの変数も差し替えられる >>> test() 2 1 |
mock.patch.dict
patch.dictは他のパッチとは異なり、Mockオブジェクトを差し込むのではなく対象ブロックにて対象dictの要素を書き換えます。
>>> d = {'a': 1} >>> print(d, id(d)) {'a': 1} 140638128425792 >>> with mock.patch.dict(d, {'b': 2, 'c': 3}): ... print d, id(d) {'a': 1, 'c': 3, 'b': 2} 140638128425792 >>> print(d, id(d)) {'a': 1} 140638128425792 >>> with mock.patch.dict(d, {'b': 2, 'c': 3}, clear=True): ... print(d, id(d)) {'c': 3, 'b': 2} 140638128425792 |
アドレス(オブジェクト)が変わらないというのが重要なポイントです。
次のページでは少し特殊なケースの利用ケースを紹介します。
イミュータブルなオブジェクトをパッチしたい
Pythonには書き換え不可能なオブジェクトが存在します。
テスト対象として一番よく遭遇するのはdatetime(またはdate)でしょう。
今回は「datetime.now()」が返却する日時を固定化させたいというシナリオです。
>>> from datetime import datetime >>> # datetimeオブジェクトは変更できない >>> with mock.patch('__main__.datetime.now', return_value=datetime(2015, 1, 1)): ... datetime.now() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/site-packages/mock/mock.py", line 1460, in __enter__ setattr(self.target, self.attribute, new_attr) TypeError: can't set attributes of built-in/extension type 'datetime.datetime' >>> # datetimeオブジェクト自体をMockオブジェクトと差し替えた上で、それぞれのオブジェクトを入れていく >>> with mock.patch('__main__.datetime', **{'now.return_value': datetime(2015, 1, 1)}): ... datetime.now() datetime.datetime(2015, 1, 1, 0, 0) |
さて、お気づきの通りこのやり方には少々欠点があります。ほかの属性が巻き込まれるのです。
datetime.now以外にdatetime.strptimeも同じ関数内で使っているなんてよくあることですよね。
datetimeを差し替えるとdatetime.stftimeもMockオブジェクトを返却するようになってしまいます。
現実的な解決方法は使われている属性をすべて定義してしまうことです。
>>> with mock.patch('__main__.datetime', **{ ... 'now.return_value': datetime(1, 1, 1), ... 'strptime.return_value': datetime(1, 2, 3), ... }): ... datetime.now() ... datetime.strptime(2000, '%Y') datetime.datetime(1, 1, 1, 0, 0) datetime.datetime(1, 2, 3, 0, 0) |
また、今回のようにdatetime.now, date.todayを固定化するだけであればtestfixturesというライブラリを使うのが理想的です。
$ pip install testfixtures
複数のオブジェクトをパッチしたい
単純にパッチしたいオブジェクトの分だけ繰り返せばよいのですが、記述方法については多少コツのようなものがあります。
withをネストする
date.todayとdatetime.now()を同時に書き換えてみましょう。
Python2だとnested関数を使うのが一般的。
>>> from contextlib import nested >>> from datetime import date, datetime >>> >>> with nested( ... mock.patch('__main__.datetime'), ... mock.patch('__main__.date') ... ) as (m, n): ... print(m, n) (<MagicMock name='datetime' id='7696572181200'>, <MagicMock name='date' id='7696572558416'>) |
Python3ではこのように書く。
>>> from unittest import mock >>> from datetime import date, datetime >>> with mock.patch('__main__.datetime') as m, mock.patch('__main__.date') as n: ... print(m, n) <MagicMock name='datetime' id='358415501576'> <MagicMock name='date' id='358416373912'> >>> # そもそもnestedがない! >>> from contextlib import nested Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> from contextlib import nested ImportError: cannot import name 'nested' |
その他、3.3からはcontextlib.ExitStackなるものがあるらしいので気になる方は触ってみるといいのではないでしょうか
あつおさんの記事
デコレータをネストする
patchを関数に対してかけたい場合は、デコレータとして利用します。
パッチされたMockオブジェクトが引数として渡されるのですが、重要なのはその順番です。
>>> from datetime import date, datetime >>> @mock.patch('__main__.date') ... @mock.patch('__main__.datetime') ... def test_patch(mock_datetime, mock_date): ... print(mock_datetime, mock_date) >>> test_patch() (<MagicMock name='datetime' id='7696571704656'>, <MagicMock name='date' id='7696571734288'>) |
withが書いた順に渡されるのに対し、デコレータでは内側(深いほう)が先に渡されます。
これは内側のデコレータから順に対象関数を包んでいくからです。
with …() as xにMockオブジェクトを差し込みたい
「with open(‘test.txt’) as f」としている箇所で「test.txt」の内容を書き換えたいことがあるかもしれません。
その場合は以下のように対処できます。
>>> with mock.patch.object(__builtins__, 'open') as m: ... m.return_value.__enter__.return_value.read.return_value = 'abc' ... with open('test.txt') as f: ... print(f.read()) abc |
ポイントは__enter__メソッドの動作を書き換えることです。
withコンテキストでは__enter__の返却値がasにわたるため、そのあとは使いそうな属性を書き換えていけばよいのです。
contextlibを使うと以下のようにも書けます。どちらがわかりやすいかは個人差がありそう。
>>> @contextlib.contextmanager ... def dummy_open(path): ... yield mock.MagicMock(**{'read.return_value': 'abc'}) ... >>> with mock.patch.object(__builtins__, 'open', side_effect=dummy_open): ... with open('test.txt') as f: ... print(f.read()) abc |
おまけ
>>> # name属性はmockの名前となる(ほとんど使わないけど) >>> m = mock.MagicMock(name='test') >>> m <MagicMock name='test' id='7696573686952'> |
くぅ~、疲れましたwこれにて完結です
あとはリファレンス読んでください!
その他、参考にした情報ソース
おかしいところがあったら優しくツッコみください