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


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

[前の記事](http://qiita.com/FGtatsuro/items/eea263bc15ebfb9767cd)で書いたmockを使用した際に、2点ほどハマった。

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

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

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

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

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

```python:外部ライブラリのモック(誤り)
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`)を使用しているため、これをモックしてやらなければいけない。

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

from mock import patch

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

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

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

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

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

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

from mock import patch

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

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

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

```python:指定した対象の属性がモック化される例
>>> 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の返り値)は指定できない。

```python:プロパティに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に入れ替えるだけでは結果が変わらない。

```python:プロパティに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をセットする必要がある。

```python:プロパティに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

## 結論

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