[python] まだmockで消耗してるの?mockを理解するための3つのポイント - くろのて
2023-03-03

[python] まだmockで消耗してるの?mockを理解するための3つのポイント

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

この記事は 2015 tech-yuruyuru アドベントカレンダー - 15日目の記事です。 2015 tech-yuruyuru アドベントカレンダー (2015/12/01 00:00〜)# 2015 #tech-yuruyuru アドベントカレンダー #tech-yuruyuru のアドベントカレンダーです。 テーマは特に決まっていません。好きなことを書いてください。 * 参加したい日の参加枠に参加登録してください * 2 日以上参加したい場合は、フィードで宣言してください ## カレンダー 1. @pjxiao: VPC のプライベートサブネットについて解説 2. @pjxiao: mDNS を使いローカルマシン内の仮想環境に接続する 3. @pjxiao: Ansible の Iventory file についておさらい 4. @p...https://connpass.com/event/22759/

モックって何よ?

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クラスを継承した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_valueside_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の 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と戯れる## 導入 Pytho...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

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 のほうが厳しく値の代入も禁じられています。

  • What is spec and spec_setI am using Mock 1.0.1 python. In the path function definition there are two optional arguments names spec and spec_set (also auto_spec) patch(target, new=DEFAULT, spec=None, create=False, 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 など専用のライブラリを使うのが望ましいです。(が今回はやりません) GitHub - spulec/freezegun: Let your Python tests travel through timeLet your Python tests travel through time. Contribute to spulec/freezegun development by creating an account on GitHub.https://github.com/spulec/freezegun GitHub - simplistix/testfixtures: testfixtures is a collection of helpers and mock objects that are useful when writing automated tests in Python.testfixtures is a collection of helpers and mock objects that are useful when writing automated tests in Python. - GitHub - simplistix/testfixtures: testfixtures is a collection of helpers and mock...https://github.com/simplistix/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

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オブジェクトとして扱われるようです。(よくしらない)

おまけ

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これにて完結です!

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

参考

unittest.mock --- 入門Mock を使う: Mock のパッチ用メソッド: 一般的な Mock の使い方の中には次のものがあります: メソッドにパッチを当てる, オブジェクトに対するメソッド呼び出しを記録する. システムの他の部分からメソッドが正しい引数で呼び出されたかどうかを確認するために、そのオブジェクトのメソッドを置き換えることができます:>>> real = SomeClass() >>> real.met...https://docs.python.org/3/library/unittest.mock-examples.html

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