LoginSignup
2
0

More than 1 year has passed since last update.

pytestで曖昧だったこと(Mock,spec,wrap)

Posted at

はじめに

自作のclassをモックする際に、調べたことをメモる。

クラス自体をモックする

  • mocker.patch(’モジュール名.Class名’, autospec=True) でモックする
    • 返り値にmockオブジェクトが取得できる
    • return_value, side_effect, assert_called系メソッドが利用できる
    • autospecを利用することで、モック対象のClassの属性・操作を模倣できる。定義なしにアクセスすると例外発生させることができる。
  • mock = mocker.Mock(spec=モジュール名.Class名) で一回、mockを定義する
    • そして、mocker.patch(’モジュール名.Class名’, mock) でモックする
    • このときpatchからの返り値もモックオブジェクトが取得でき、第2引数で指定したmockと同一変数
  • mocker.patch.object()ではできないのか、ちょっと気になる。本来は、オブジェクトのメソッドもモックする用途だと思うのでその理解に留める。(patchでもメソッドのモックはできるが、、)

プロパティの理解がむずい件

ドキュメントはあるが、イメージしづらい。また、ググると各々の実装パターンが散見される。

spec について調べたいときに、patch()のセクションには基本的なことが記載されていない認識。 Mock のセクションを見ると、なんとか分かる内容がみつかる。

spec: モックオブジェクトの仕様として働く文字列のリストもしくは存在するオブジェクト (クラスもしくはインスタンス) を指定します。オブジェクトを渡した場合には、 dir 関数によって文字列のリストが生成されます (サポートされない特殊属性や特殊メソッドは除く) 。このリストにない属性にアクセスした際には [AttributeError](https://docs.python.org/ja/3/library/exceptions.html#AttributeError) が発生します。

spec が (文字列のリストではなく) オブジェクトの場合、 [__class__](https://docs.python.org/ja/3/library/stdtypes.html#instance.__class__) はスペックオブジェクトのクラスを返します。これによってモックが [isinstance()](https://docs.python.org/ja/3/library/functions.html#isinstance) テストに通るようになります。

spec_set:
より厳密なやつ。

spec_set: より厳しい spec です。こちらを利用した場合、 spec_set に渡されたオブジェクトに存在しない属性に対し 設定 や取得をしようとした際に [AttributeError](https://docs.python.org/ja/3/library/exceptions.html#AttributeError) が発生します。

autospec, spec, spec_setの使う基準

個人的にはこの順番で考えている。

  1. 基本的に、 autospec を使う
  2. 何らかの不都合がある場合、 spec の使用を検討する
  3. 明確に理由があるならば、 spec_set で厳しくコントロールする

Mockのwrapsオプション

wrapsを指定しているコードを時々見られるが、挙動がよくわかっていなかった。

説明を確認したところ、取り扱いが難しいと感じている。

Mockでモックしたとしても、wrapsを指定していると実際のモック対象のメソッドが呼ばれる挙動になるので、その挙動を認識していないと、想定外の動作になる。

モック対象と同等の属性でモックしたいという目的であれば、spec系のオプションを利用するべきだと思う。。ならば、wrapsはどういった用途で使うのだろうか。。。。(調べる必要あり)

return_valueを設定すれば、ピンポイントでモックできるので細かい要件で使うのだろうか。。

上記要件であれば、mocker.patch.objectで対象のメソッドをピンポイントでモックすればよいだろう。

結局、wrapsの用途は見えてこない。。

wraps: ラップするモックオブジェクトを指定します。 もし wraps に None 以外を指定した場合には、モックの呼び出し時に指定したオブジェクトを呼び出します (戻り値は実際の結果を返します)。 属性へのアクセスは、ラップされたオブジェクトと対応するモックを返します (すなわち、存在しない属性にアクセスしようとした場合は [AttributeError](https://docs.python.org/ja/3/library/exceptions.html#AttributeError) が発生します)。

return_value が設定されている場合には、ラップ対象は呼び出されず、 return_value が返されます。

一応、検証に利用したコード

 tree -L 3 .                                                                1 
.
├── app.py
├── classmodule
   └── hoge.py
└── test_app.py

app.Hogeでモックしているのは、app.pyでfrom〜importしているから。

# test_app.py
import pytest
import app

@pytest.fixture()
def mock_Hoge(mocker):
    # mock = mocker.Mock(spec=app.Hoge)
    # mocker.patch('app.Hoge', mock)
    mock2 = mocker.patch('app.Hoge', autospec=True)
    return mock2
    # return mock

def test_app(mock_Hoge):
    app.main()
    mock_Hoge.foo.assert_called_once()
    # mock_Hoge.fuga()
    # mock_Hoge.fuga
# app.py
from classmodule.hoge import Hoge

def main():
    Hoge.foo()
# hoge.py
class Hoge:
    @staticmethod
    def foo():
        print(Hoge.foo.__name__)

    def bar(self):
        print(Hoge.bar.__name__)
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0