隣の席の人がテスト強化週間とか抜かしていたので自分もちゃんと理解するために なるべくわかりやすく まとめてみようと思います。
この記事は 2015 tech-yuruyuru アドベントカレンダー - 15日目の記事です。 http://connpass.com/event/22759/
モックって何よ?
mockは特定のオブジェクトの代理をしてユニットテストを円滑に進めるためのモジュールです。
python3.3からはビルトインに入りましたが、それ未満のバージョンではインストールが必要です。
以下のようにインストールしてください。
$ pip install mock
-
インストールしたmockを使う場合は単に
import mock
とすればよいのですが ビルトインmockを使う場合は、from unittest import mock
のようにして使うのが一般的です。 -
以降、この記事では無用な混乱を避けるため、mockのimport文を省略します。使い方は概ね同じはずです。
個人的にmockを理解する上で重要なポイントは以下の3点だと考えています。
- (大体)どんな振る舞いも表現できる Mockオブジェクト
- 任意の名前空間に自身の 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='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__
定義しない属性を参照しても AttributeError は発生せず新たな Mock オブジェクトが得られているのがわかりますね。 この仕組みによって数珠つなぎに存在しない属性を作成できるというわけです。
また、初期化する段階で属性を辞書のキーとして指定できます。
これは 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
モックの実行
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
という引数(属性)を持っています。直訳すると「副作用」です。
一般的に副作用を持つ関数とは、状態を持ち同じ引数に対する実行結果の同一性が保証されないことを指します。
モックに関して言うと、通常は常に同じ返却値を返しますが 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('インデックスエラー',)
- info
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='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
call_args_listを直接参照するのは面倒なので、 呼び出されたときの引数をテストするためのメソッドがいくつか用意されています。 通常はこちらを使うと良いでしょう。
>>> o = mock.MagicMock()
>>> # 先に2回呼び出しておく
>>> o(1, b=2)
<MagicMock name='mock()' id='140232966996560'>
>>> o(3, c=4)
<MagicMock name='mock()' id='140232966996560'>
>>> # 直前に呼び出されたかどうかを確認する場合
>>> 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
>>> # 呼び出しを厳密にチェックしたい場合(順序を無視するならany_order=Falseを指定する)
o.assert_has_calls([mock.call(1, b=2), mock.call(3, c=4)])
大抵はユニットテストのテストケースを後述する パッチ機能 で書き換え、期待通りに呼び出されているかの検証に利用されます。
MagicMock と Mock の違いについて
MagicMockの Magic
は MagicMethod
の
Magic です。
通常の 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というわけです。
http://kimihiro-n.appspot.com/show/5837449502654464
パッチ機能
パッチ機能によってオブジェクトの要素を差し替えるわけですが、パッチは決して魔法ではありません。
使いこなすには多少のモジュールの知識が必要です。
まずはmockを使わずに関数のふるまいを変えてみましょう。
- b.py
- console
-
# coding: utf-8 import os def dummy(*args, **kwargs): return 'dummy' os.path.join = dummy
-
>>> import os >>> os.path.join('/a/b/c', 'd/e') '/a/b/c/d/e' >>> import b >>> os.path.join('/a/b/c', 'd/e') 'dummy'
上記の例を解説すると、「consoleで参照している 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は第1位置引数で指定したモジュールパス(文字列)が指すオブジェクトを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, sys.copyright, dummyright)
<module 'sys' (built-in)> おれ おれ
with ステートメントで書き換えた後の値は as
で受け取れるので比較用に二重に定義する必要はありません。
デコレータを使って Patch する
この記事では基本的に狭い範囲に適用しやすいコンテキストマネージャを使いますが、 実際のコードではテストケースのメソッドや関数にデコレータを設定するほうが多いと思います。
具体的には次のように書きます。
>>> 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.'
引数についてもう少し解説します。
注意点としてぜひ覚えておいていただきたいのは第2引数の new
です。これは第1引数で指定した対象(モジュールパス)のオブジェクトを置き換える値で、
省略すると自動的にモックオブジェクトとなり、そのモックオブジェクトは対象関数の仮引数に追加されます。
つまりこれの有無によってデコレータとして使用したときの挙動が変わります。
もう少し具体的に言うと、増えないと思ってた引数が実は必要な(仮引数が不足)パターンと、増えると思っていた引数が実は不要な(仮引数が過剰)パターンがあるわけですが、
特に後者の場合は pytest と併用していると fixture 'xxxx' not found
のようにフィクスチャがないよーみたいなエラーがでて、
pytest関連のエラーだと思って明後日の方向に調査をしてしまう方がいらっしゃいます👀。正直これはpytestのミスリードだと思います。
とりあえずパターンを認識しておきましょう。
>>> # OK
>>> @mock.patch('sys.copyright')
... def test1(mocked_copyright):
... pass
>>> test1()
>>> # OK
>>> @mock.patch('sys.copyright', "くろのて")
... def test2():
... pass
>>> test2()
>>> # NG (仮引数不足なパターン)
>>> @mock.patch('sys.copyright')
... def test3():
... pass
>>> test3()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.11/unittest/mock.py", line 1359, in patched
return func(*newargs, **newkeywargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: test3() takes 0 positional arguments but 1 was given
>>> # NG (仮引数過剰なパターン)
>>> @mock.patch('sys.copyright', "くろのて")
... def test4(mocked_copyright):
... pass
>>> test4()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.11/unittest/mock.py", line 1359, in patched
return func(*newargs, **newkeywargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: test4() missing 1 required positional argument: 'mocked_copyright'
# pytest を使っているとこのエラーが fixture 'mocked_copyright' not found になることがある
個人的にこの仕様は混乱を招くのであまりよいとは思いませんが、new引数に指定した値は差し替わる実引数と同じ値になり無意味なので省略したのではないかと勝手に推察しています。
- info
- 位置引数とか仮引数とかよくわからんぞという方はこちらの記事もおすすめです。古い記事ですが内容に問題はありません。
- https://note.crohaco.net/2014/python-argument-intro/
Mockをもう少し厳しくする
さて、先程Mockオブジェクトのことをその性質から高機能粘土と言いましたが、基本的に彼は紳士なので何をしても怒りません。 本来はエラーになるはずの存在しない引数呼び出しも優しく受け容れてくれます。
それでは物足りないと思った方は autospec
引数を指定しましょう。これでモックは小うるさくなります。
autospecはモック対象オブジェクトの属性をコピーし、呼び出し時にはその引数が適切かどうか(シグネチャという)をチェックします。
>>> class A:
... a = 1
... b = 2
...
... def __init__(self, c):
... self.c = c
>>> a = A(3)
>>> @mock.patch('__main__.a')
... def test6(mocked):
... print(a.a, a.b, a.c)
...
>>> test6()
<MagicMock name='a.a' id='140231890542928'> <MagicMock name='a.b' id='140232966694800'> <MagicMock name='a.c' id='140232696230096'>
>>> # aはz属性を持たないためエラーになる
>>> @mock.patch('__main__.a', autospec=True)
... def test7(mocked):
... print(a.a, a.b, a.z)
>>> test7()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1256, in patched
return func(*args, **keywargs)
File "<stdin>", line 3, in test7
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 599, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'z'
>>> @mock.patch('__main__.A')
... def test8(Mocked):
... a = A(z=1000)
... print(a.a, a.b, a.z)
>>> test8()
<MagicMock name='A().a' id='140232967090704'> <MagicMock name='A().b' id='140232967273808'> <MagicMock name='A().z' id='140232967040592'>
>>> # Aはz引数を受け取らないためエラーになる
>>> @mock.patch('__main__.A', autospec=True)
... def test9(Mocked):
... a = A(z=1000)
>>> test9()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1256, in patched
return func(*args, **keywargs)
File "<stdin>", line 3, in test9
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1015, in __call__
_mock_self._mock_check_sig(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 113, in checksig
sig.bind(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 3015, in bind
return args[0]._bind(args[1:], kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 2930, in _bind
raise TypeError(msg) from None
TypeError: missing a required argument: 'c'
>>> # 動的に生成する属性は検知できない
>>> @mock.patch('__main__.A', autospec=True)
... def test10(Mocked):
... a = Mocked()
... print(a.c)
...
>>> test10()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1256, in patched
return func(*args, **keywargs)
File "<stdin>", line 3, in test10
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1015, in __call__
_mock_self._mock_check_sig(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 113, in checksig
sig.bind(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 3015, in bind
return args[0]._bind(args[1:], kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 2930, in _bind
raise TypeError(msg) from None
TypeError: missing a required argument: 'c'
- info
- 基本的には autospec を使えばよいですが、他にも spec, spec_set という引数があります。正確に言うとこれはモックオブジェクトの引数で、MagicMockに渡されます。
- いずれもオブジェクトが指定された場合、そのオブジェクトにない属性へのアクセスを制御するものですが、spec_set のほうが厳しく値の代入も禁じられています。
- https://stackoverflow.com/questions/25323361/what-is-spec-and-spec-set
- patchに指定する
spec=True
と、autospec=True
の違いはなにかといえば、生成されたオブジェクトにspecがあるかないかです。- 前者は、specのない単なるMagicMockとなる
- 後者は specのある型が厳密なMagicMockとなる
>>> # spec=Trueで作った場合、mocked.cはspecを持たないため、本来存在しない属性(mocked.c.z)も再帰的に辿れる >>> @mock.patch('__main__.a', spec=True) ... def test11(mocked): ... print(mocked.a, mocked.c, type(mocked)) ... print(mocked.c.z) ... >>> test11() <MagicMock name='a.a' id='140231890769424'> <MagicMock name='a.c' id='140233233710032'> <class 'unittest.mock.NonCallableMagicMock'> <MagicMock name='a.c.z' id='140231890732176'> >>> # autospec=Trueで作った場合、mocked.cはspecを持つため、本来存在しない属性(mocked.c.z)にはアクセスできない >>> @mock.patch('__main__.a', autospec=True) ... def test12(mocked): ... print(mocked.a, mocked.c, type(mocked)) ... print(mocked.c.z) ... >>> test12() <NonCallableMagicMock name='a.a' spec='int' id='140231890768016'> <NonCallableMagicMock name='a.c' spec='int' id='140232427657744'> <class 'unittest.mock.NonCallableMagicMock'> Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1256, in patched return func(*args, **keywargs) File "<stdin>", line 4, in test12 File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 599, in __getattr__ raise AttributeError("Mock object has no attribute %r" % name) AttributeError: Mock object has no attribute 'z'
他の引数は別の機会にということで...(多分やらない)
from importを使ったオブジェクトを Patch する
モジュールの理解があれば特筆するようなことでもないのですが、ハマりポイントなような気がするので一応解説しておきます。
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') as m:
... 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
(2023/01/20追記) 2番目の with文の as m が抜けていました。 ご報告いただいた方、ありがとうございました。
mock.patch.object
mock.patchがモジュールパスに該当するオブジェクトを差し替えるのに対し、
patch.object
は任意のオブジェクトの属性を差し替えます。
>>> class Test(object):
... a = 100
>>> # Test.aを200で書き換える
>>> with mock.patch.object(Test, 'a', 200) as test:
... print(Test.a)
... print(test)
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
ちなみに引数は第1引数の型を除き mock.patch と同じです。
さて、mock.patchではなくmock.patch.objectを使うべきケースはどのようなものがあるのでしょうか。
Globalスコープに属さない変数を差し替える場合
mock.patchが差し替えられるのは globalスコープ に属する変数に限られます。
>>> class Test(object):
... a = 1
>>> test1 = Test()
>>> test2 = Test()
>>> # globalスコープに変数が定義されている場合は以下のようにできる
>>> with mock.patch('__main__.test1', a=2):
... print(test1.a)
... print(test2.a)
2
1
>>> # ローカル変数はモジュールパスを指定できないため対応不可
>>> def test():
... test1 = Test()
... test2 = Test()
... with mock.patch('test1', a=2):
... print(test1.a)
... print(test2.a)
>>> test()
TypeError: Need a valid target to patch. You supplied: 'test1'
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)
現在日時を固定するのであれば、freezegun や testfixtures など専用のライブラリを使うのが望ましいです。(が今回はやりません) https://github.com/spulec/freezegun https://github.com/Simplistix/testfixtures
複数のオブジェクトをパッチしたい
単純にパッチしたいオブジェクトの分だけ繰り返せばよいのですが、記述方法については多少コツのようなものがあります。
withをネストする
date.today
と datetime.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
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='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はデコレータでもかけるという話と、そのモックオブジェクト引数の有無について話しました。
複数ある場合に大事なのはテスト対象の引数順で、デコレータでは内側(深いほう)が先に渡されます。 これは内側のデコレータから順に対象関数を包んでいくという言語の性質的な理由からです。
>>> 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'>)
- info
- ちなみにpytestのフィクスチャと併用する場合、モックオブジェクトが前でフィクスチャが後ろです。
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オブジェクトとして扱われるようです。(よくしらない)
.@podhmo こういう感じhttps://t.co/89eifvQDdS
— po (@podhmo) December 15, 2015
おまけ
mock.ANY というものがあります。 これは何とでもマッチする単純なオブジェクト ですが結構有能なイケメンです。
単純に考えるとそんなものテストしないだろって思うかもしれませんが、listやdictのような複合的な要素からなるオブジェクトの比較に使えます。 例えば、作成日時フィールドだけは無視したいとかよくありますよね。リストの方はいい例が思いつかなかったけど、座標計算結果のテストで特定の軸を無視するとか。
>>> assert [1, 2, 3] == [1, mock.ANY, 3]
>>> assert {"a": 1, "b": 2, "created_at": 1577804400} == {"a": 1, "b": 2, "created_at": mock.ANY}
わざわざ、個別にassertしていた方は明日から mock.ANY でドヤ顔しましょう。
くぅ~、疲れましたwこれにて完結です!
あとはリファレンス読んでください!
参考
https://docs.python.org/ja/3/library/unittest.mock-examples.html
おかしいところがあったら優しくツッコミください