[python] まだmockで消耗してるの?mockを理解するための3つのポイント
2015-12-15
2018-09-04

隣の席の人がテスト強化週間とか抜かしていたので自分もちゃんと理解するために なるべくわかりやすく まとめてみようと思います。

この記事は 2015 tech-yuruyuru アドベントカレンダー - connpass 15日目の記事です。

目次

What is the mock?

mockは特定のオブジェクトの代理をしてユニットテストを円滑に進めるためのモジュールです。

python3.3からはビルトインに入りましたが、それ未満のバージョンではインストールが必要です。

以下のようにインストールしてください。

$ pip install mock

Note

インストールしたmockを使う場合は単に import mock とすればよいのですが

ビルトインmockを使う場合は、 from unittest import mock のようにして使うのが一般的です。

(以降、この記事では無用な混乱を避けるため、mockのimport文を省略します。使い方は概ね同じはずです)

個人的にmockを理解する上で重要なポイントは以下の3点だと考えています。

  1. (大体)どんな振る舞いも表現できる Mockオブジェクト

  2. 任意の名前空間に自身の Mockオブジェクト をねじ込むことができる パッチ機能

  3. ねじ込んだ Mockオブジェクト がどのように使われたか(呼び出されたか)を記録する キャプチャ機能

これらが合わさるからこそmockは強力なわけですが、一遍には理解しづらい概念かもしれません。

一つ一つ理解していきましょう。

Mock object

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='4502806368'>

>>> m.c.d
<MagicMock name='mock.c.d' id='4504775928'>

>>> m.e.f.g
<MagicMock name='mock.e.f.g' id='4504837984'>

>>> m.__a
<MagicMock name='mock.__a' id='4504854824'>

>>> # 特殊属性は自動生成できない
>>> m.__a__
Traceback (most recent call last):
  File "", line 1, in
  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 "", 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='4504971528'>

>>> # 初期化後であればconfigure_mockメソッドを使うこともできます
>>> m.configure_mock(**{'e.f.g': 200, 'h.i': 300})
>>> m.e.f.g
200
>>> m.h.i
300

Execution mock

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 0x10c84ba58>

>>> m()
10
>>> m()
11
>>> m()
12
>>> m()
Traceback (most recent call last):
  File "", line 1, in
  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 "", line 1, in
  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 "", line 1, in
  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('インデックスエラー',)

Note

return_valueside_effect を同時に指定した場合 side_effect が優先されます。

もし、あとからside_effectを無効化する場合はNoneを代入するという手もあります。(そんなことはしないほうがいいんだけど)

Capture

少々順番が前後しますが、タイミング的にここがベストな気がするので説明します。

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='4505008952'>
>>> n.called
True
>>> # 一度だけ呼び出されたかを確認するときにはassert_called_once_withを使う
>>> n.assert_called_once_with()
>>> n()
<MagicMock name='mock()' id='4505008952'>
>>> n.assert_called_once_with()
Traceback (most recent call last):
  File "", line 1, in
  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)

>>> o(3, c=4)

>>> # 直前に呼び出されたかどうかを確認する
>>> o.assert_called_with(1, b=2)
Traceback (most recent call last):
  File "", line 1, in
  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 "", line 1, in
  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の MagicMagicMethodMagic です。

通常の Mock ではサポートされていない四則演算等も MagicMock なら実現できます。

>>> m = mock.Mock()
>>> m + 1
Traceback (most recent call last):
  File "", line 1, in
TypeError: unsupported operand type(s) for +: 'Mock' and 'int'
>>> n = mock.MagicMock()
>>> n + 1
<MagicMock name='mock.__add__()' id='4312098744'>
>>> n + 2
<MagicMock name='mock.__add__()' id='4312098744'>
>>> m[1]
Traceback (most recent call last):
  File "", line 1, in
TypeError: 'Mock' object does not support indexing
>>> n[1]
<MagicMock name='mock.__getitem__()' id='4312197552'>
>>> m['a']
Traceback (most recent call last):
  File "", line 1, in
TypeError: 'Mock' object is not subscriptable
>>> n['a']
<MagicMock name='mock.__getitem__()' id='4312197552'>
>>> for i in m: i
...
Traceback (most recent call last):
  File "", line 1, in
TypeError: 'Mock' object is not iterable
>>> for i in n: i
...

当たり前ですが、通常のMockには一つもマジックメソッドが存在しないというわけではありません。

明示的に実装しなければならないマジックメソッドがあらかじめ実装されているのがMagicMockというわけです。

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

Patch

パッチ機能によってオブジェクトの要素を差し替えるわけですが、パッチは決して魔法ではありません。

使いこなすには多少のモジュールの知識が必要です。

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

b.py

__main__

# coding: utf-8
import os
def dummy(*args, **kwargs):
    return 'dummy'

os.path.join = dummy
>>> import os
>>> os.path.join

>>> os.path.join('/a/b/c', 'd/e')
'/a/b/c/d/e'
>>> import b
>>> os.path.join

>>> os.path.join('/a/b/c', 'd/e')
'dummy'

上記の例を解説すると、 __main__ で参照している osb.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 を返却します。

mock.patch は 書き換え後の値が関数以外であっても親オブジェクトの関係を損なわずに書き換えてくれます。(2018/2/5追記)

>>> with mock.patch('sys.copyright', 'おれ') as dummyright:
...     import sys
...     print(sys.copyright, dummyright)

おれ おれ

with ステートメントで書き換えた後の値は as で受け取れるので比較用に二重に定義する必要はありません。

Note

当該記事では 狭い範囲に適用しやすい コンテキストマネージャを多用していますが、 実際のコードではテストケースのメソッドや関数にデコレータを設定するほうが多いと思います。

具体的には次のように書きます。

>>> import sys

>>> sys.copyright
'Copyright (c) 2001-2018 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved.'

>>> @mock.patch('sys.copyright', '俺やで!')
... def test():
...     import sys
...     print(sys.copyright)
...

# 実際に呼び出すのはテストランナーがやるので本来は考えなくて良い
>>> test()
俺やで!

>>> sys.copyright
'Copyright (c) 2001-2018 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved.'

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='4312206192'>
200

>>> Test.a
100

さて、mock.patchではなくmock.patch.objectを使うべきケースはどのようなものがあるのでしょうか。

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 "", line 1, in
  File "", 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:  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 "", line 1, in
  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.todaydatetime.now() を同時に書き換えてみましょう。

>>> 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='4346916112'>, <MagicMock name='date' id='4353610192'>)

Warning

contextlib.nestedは現在推奨されていないらしいです。

thanks tell-k

Python2.7以降(Python 3)ではこのように書く。

>>> 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='4346915536'>, <MagicMock name='date' id='4353660944'>)

>>> # そもそもnestedがない!
>>> from contextlib import nested
Traceback (most recent call last):
  File "", line 1, in
    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='4357884816'>, <MagicMock name='date' id='4353612240'>)

Note

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

openに関しては mock_open というのがあり、fileオブジェクトとして扱われるようです。

おまけ

>>> # name属性はmockの名前となる(ほとんど使わないけど)
>>> m = mock.MagicMock(name='test')
>>> m
<MagicMock name='test' id='4505025616'>

くぅ~、疲れましたwこれにて完結です

あとはリファレンス読んでください!

参考

(どんどんリンクが切れていくんだが..

おかしいところがあったら優しくツッコみください