87
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

そんなpatchで大丈夫か? (mockについてのメモ〜後編〜)

Posted at

大丈夫じゃない。問題だ。

前の記事で書いたmockを使用した際に、2点ほどハマった。

importの仕方によりpatch対象を変える必要がある

以下のように外部ライブラリを使用しているモジュールをテストする場合を考える。(以降の例では、httpライブラリrequestsを外部ライブラリとして使用している。以降の記述の何点かは、requestsに依存した内容になっている。)
このモジュール単体をテストするためには、外部ライブラリのクラスをモックすれば良い。

テスト対象のモジュール(A.target.pyとする)
from requests import Session

# Sessionクラスをどこかで使用する
s = Session()

ここで、以下のように書いた場合、上手く外部ライブラリがモックできない。

外部ライブラリのモック(誤り)
import A

from mock import patch

# A.target内で使用しているSessionクラスをモックしている(つもり)
with patch('requests.Session'):
	# テスト    

ポイントはテスト対象モジュール内での外部ライブラリのimportの仕方にある。

from requests import Sessionとした場合、Sessionクラスはimportを実行したモジュール空間下に置かれる。
テスト対象モジュールがA.target.pyの場合、A.target.Sessionとなるわけだ。

テスト対象モジュールは、自分の配下にimportしたSessionクラス(A.target.Session)を使用しているため、これをモックしてやらなければいけない。

外部ライブラリのモック(正しい)
import A

from mock import patch

# A.target内にimportしたSessionクラスをモックしている
with patch('A.target.Session'):
	# テスト    

一方、import requestsとしていた場合はどうか?

テスト対象のモジュール(fromを使用しない場合)
import requests

# Sessionクラスをどこかで使用する
s = requests.Session()

今度はSessionクラスを自分の配下にimportせずに、requests.Sessionと外部ライブラリのパッケージを指定している。
なので、モック対象もA.target.Sessionではなくrequests.Sessionとなる。

外部ライブラリのモック(fromを使用しない場合)
import A

from mock import patch

# requestsパッケージ内のSessionクラスをモックしている
with patch('requests.Session'):
	# テスト    

プロパティのモックをする際の注意

patchでは、指定した対象の属性(メソッドなど)も自動的にモックされる。この際には、MagicMockというMockクラスがデフォルトで使用される。

指定した対象の属性がモック化される例
>>> import requests
>>> from mock import patch

# requests.Sessionクラスのpostメソッドがモックされている
>>> with patch('requests.Session'):
    s = requests.Session()
    print(s.post)
...
<MagicMock name='Session().post' id='4401343792'>

上記で自動的にモックされたrequests.Sessionクラスのpostメソッドの戻り値として、レスポンスのモックを設定する場合を考える。
postメソッドのレスポンスは'text'プロパティを持つため、作成するモックレスポンスにも同じプロパティ(のモック)をセットするものとする。

プロパティをモックする際にはPropertyMockを使用する。MagicMockを使用しても、プロパティの返り値(=getterの返り値)は指定できない。

プロパティにMagicMockを使用する(失敗)
>>> import requests
>>> from mock import patch, MagicMock

>>> with patch('requests.Session'):
    s = requests.Session()
    response = MagicMock()
    # 'text'プロパティをMagicMockでモック(しようとする)
    text = MagicMock()
    text.return_value = 'property'
    response.text = text
    s.post.return_value = response
    print(s.post().text)
...

# text.return_valueに指定した値は返らない
<MagicMock name='Session().post().text' id='4506288536'>

しかし、単純にMagicMockをPropertyMockに入れ替えるだけでは結果が変わらない。

プロパティにPropertyMockを使用する(失敗)
>>> import requests
>>> from mock import patch, MagicMock, PropertyMock

>>> with patch('requests.Session'):
    s = requests.Session()
    response = MagicMock()
    # MagicMockではなくPropertyMockを使用する
    text = PropertyMock()
    text.return_value = 'property'
    response.text = text
    s.post.return_value = response
    print(s.post().text)
...

# まだ上手くいかない...
<PropertyMock name='Session().post().text' id='4506377240'>

これはモックインスタンス(上記の例ではレスポンスのモック)に対してPropertyMockをセットしようとした場合に起こる問題である。
この場合、モックオブジェクト自体ではなく、そのtypeオブジェクトにPropertyMockをセットする必要がある。

プロパティにPropertyMockを使用する(成功)
>>> import requests
>>> from mock import patch, MagicMock, PropertyMock

>>> with patch('requests.Session'):
    s = requests.Session()
    response = MagicMock()
    text = PropertyMock()
    text.return_value = 'property'
    # 別のモックにPropertyMockをセットする場合、モックオブジェクト自体ではなく、そのtypeオブジェクトにセットする
    type(response).text = text
    s.post.return_value = response
    print(s.post().text)
...
property

ここまでいろいろ書いたけれど

どっちもちゃんとドキュメントに書いてあった...

http://www.voidspace.org.uk/python/mock/patch.html#where-to-patch
http://www.voidspace.org.uk/python/mock/mock.html?highlight=propertymock#mock.PropertyMock

結論

ドキュメントちゃんと読みましょう。

87
59
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
87
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?