[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点だと考えています。

これらが合わさるからこそ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というわけです。

Pythonの万能モック MagicMockと戯れる

パッチ機能

パッチ機能によってオブジェクトの要素を差し替えるわけですが、パッチは決して魔法ではありません。
使いこなすには多少のモジュールの知識が必要です。

まずはmockを使わずに関数のふるまいを変えてみましょう。

b.py __main__
1
2
3
4
5
6
# coding: utf-8
import os
def dummy(*args, **kwargs):
    return 'dummy'
 
os.path.join = dummy
>>> import os
>>> os.path.join
<function join at 0x7f1e752a9f50>
>>> os.path.join('/a/b/c', 'd/e')
'/a/b/c/d/e'
>>> import b
>>> os.path.join
<function dummy at 0x7f1e7525bcf8>
>>> os.path.join('/a/b/c', 'd/e')
'dummy'

上記の例を解説すると、__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
# coding: utf-8
e = 100
 
def f():
    return e
 
def g():
    def h():
        return e
    return h

(こんな深くなくてもいいんだけど

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

アドレス(オブジェクト)が変わらないというのが重要なポイントです。

次のページでは少し特殊なケースの利用ケースを紹介します。

1 2