大丈夫じゃない。問題だ。
前の記事で書いたmockを使用した際に、2点ほどハマった。
importの仕方によりpatch対象を変える必要がある
以下のように外部ライブラリを使用しているモジュールをテストする場合を考える。(以降の例では、httpライブラリrequestsを外部ライブラリとして使用している。以降の記述の何点かは、requestsに依存した内容になっている。)
このモジュール単体をテストするためには、外部ライブラリのクラスをモックすれば良い。
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
としていた場合はどうか?
import requests
# Sessionクラスをどこかで使用する
s = requests.Session()
今度はSessionクラスを自分の配下にimportせずに、requests.Session
と外部ライブラリのパッケージを指定している。
なので、モック対象もA.target.Session
ではなくrequests.Session
となる。
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の返り値)は指定できない。
>>> 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に入れ替えるだけでは結果が変わらない。
>>> 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をセットする必要がある。
>>> 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
結論
ドキュメントちゃんと読みましょう。