【pytest】mocker.patch.objectを使ってみる

# 環境
+ windows 10
+ python=3.7.3
+ pytest=5.0.1
+ pytest-mock=1.10.4

# 準備
pytestをインストールします。

`$ pip install pytest`

Mockを使うため、pytest-mockもインストールします。

`$ pip install pytest-mock`

# テスト対象
以下のような、visaモジュールを使って外部の機器と通信を取るCommunicationクラスを例にとります。
Communicationクラスはopen_deviceメソッドを持っており、これのテストを考えます。

```python:src/communication.py
import visa


class Communication(object):

    def __init__(self):
        self._rm = visa.ResourceManager()
        self._device = None

    def open_device(self, device_name: str) -> bool:
        try:
            self._device = self._rm.open_resource(device_name)
        except IOError:
            return False
        else:
            return True

```

# テスト
open_deviceメソッド内で使われている、open_resource()はホストに対象機器が接続されているかどうかで挙動が変わってしまいます。なので、これをMockに置き換えます。

```python:test/test_communication.py
import pytest

from src.communication import Communication


class TestCommunication(object):

    def test_open_device(self, mocker):
        com = Communication()
        rm_mock = mocker.MagicMock()
        rm_mock.open_resource = mocker.Mock()
        with mocker.patch.object(com, "_rm", rm_mock):
            result = com.open_device("NANIKA")
            assert result is True
            rm_mock.open_resource.assert_called_once_with("NANIKA")


```

testディレクトリの一つ上の階層で、

`$ pytest`

と打つと、テストが通ることが確認できると思います。

次に、open_resource()が実行された時にIOErrorが送出されるようにside_effectを設定します。

```python:test/test_communication.py
import pytest

from src.communication import Communication


class TestCommunication(object):

    @pytest.fixture()
    def create_com(self):
        self._com = Communication()
        yield
        self._com = None

    def test_open_device(self, create_com, mocker):
        rm_mock = mocker.MagicMock()
        rm_mock.open_resource = mocker.Mock()
        with mocker.patch.object(self._com, "_rm", rm_mock):
            result = self._com.open_device("NANIKA")
            assert result is True
            rm_mock.open_resource.assert_called_once_with("NANIKA")

    def test_open_device_when_io_error(self, create_com, mocker):
        rm_mock = mocker.MagicMock()
        rm_mock.open_resource = mocker.Mock(side_effect=IOError())
        with mocker.patch.object(self._com, "_rm", rm_mock):
            result = self._com.open_device("NANIKA")
            assert result is False

```

Communicationクラスのインスタンスを作成するところは、fixtureにまとめています。

こちらもテストが通ると思います。

## ちなみに
```
rm_mock = mocker.MagicMock()
```
の部分を、

```
rm_mock = mocker.Mock()
```
に変えると、with文のところでエラーが起きます。

```
self = <test_communication.TestCommunication object at 0x0000022DB34E5080>
mocker = <pytest_mock.MockFixture object at 0x0000022DB3602BA8>

    def test_open(self, mocker):
        com = Communication()
        rm_mock = mocker.Mock()
        rm_mock.open_resource = mocker.Mock()
>       with mocker.patch.object(com, "_rm", rm_mock):
E       AttributeError: __enter__

test\test_communication.py:12: AttributeError
```

Mock()のサブクラスのMagicMock()はここら辺のデフォルト値があらかじめ設定されているようです。

# 追記(2020/03/24)
pytest-mockのバージョンを2.0.0でテストを行うと、

```python:
with mocker.patch.object():
```
の部分でValueErrorが送出されます。

```
ValueError: Using mocker in a with context is not supported. https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager
```

mockerのwithコンテキスト内での使用はサポートされなくなったようです。
withを外せば問題なくテストを行うことができます。

```python:
mocker.patch.object()
```

# 参考URL
+ https://santexgroup.com/wp-content/uploads/2014/10/mock_python.html#slide1
+ http://note.crohaco.net/2015/python-mock/
+ https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager