pytestでMockを使う

# はじめに
pytestを使用してテストコードを書いたときに、Mockの使い方に苦労したのでまとめてみました。

# Mockとは
Mockは特定のオブジェクトの代理をして、ユニットテストを円滑に進めるためのモジュールです。

# 準備
1. pytestのインストール

    ```bash
    pip install pytest pytest-mock
    ```

2. テスト対象のコードの作成

    ディレクトリ構成

    ```bash
    root
    ├─src
    │   └─script.py    ・・・テスト対象のソースコード
    └─tests
        ├─__init__.py  ・・・空ファイル
        └─script.py    ・・・テストコード
    ```

    テスト対象のソースコード

    ```python:src/script.py
    import requests


    def sample1(url):
        return sample2(url)


    def sample2(url):
        try:
            response = requests.get(url)
            return response.status_code
        except Exception:
            return 0


    class SampleClass:
        def __init__(self):
            self._req = requests

        def get(self, url):
            try:
                response = self._req.get(url)
                return response.status_code
            except Exception:
                return 0
    ```

# Mockを使用したテストの実施

## 関数をMockする
sample1関数をテストするときに、sample2関数を置き換える。

### 関数の返り値を指定する

テストコード

```python:tests/test_script.py
from src.script import sample1


def test_sample1_mock_sample2_200(mocker):
    """
    sample2関数が200を返すようする。
    """
    status_code = 200
    url = "https://hogehoge.com"
    mocker.patch("src.script.sample2", return_value=status_code)
    assert sample1(url) == status_code


def test_sample1_mock_sample2_404(mocker):
    """
    sample2関数が404を返すようする。
    """
    status_code = 404
    url = "https://fugafuga.com"
    mocker.patch("src.script.sample2", return_value=status_code)
    assert sample1(url) == status_code
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 2 items

tests\test_script.py ..                                        [100%] 

========================= 2 passed in 0.19s =========================
```

### 関数の返り値を動的に変更する

テストコード

```python:tests/test_script.py
from src.script import sample1


def test_sample1_mock_sample2(mocker):
    """
    sample2関数がurlによって200,404,0のいずれかを返すようする。
    """
    def return_status_code(url):
        """
        urlによって200,404,0のいずれかを返す
        """
        if url == "https://hogehoge.com":
            return 200
        elif url == "https://fugafuga.com":
            return 404
        else:
            return 0

    mocker.patch("src.script.sample2", side_effect=return_status_code)
    assert sample1("https://hogehoge.com") == 200
    assert sample1("https://fugafuga.com") == 404
    assert sample1("https://higehige.com") == 0
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py .                                         [100%] 

========================= 1 passed in 0.20s =========================
```

### 関数呼び出し時にExceptionを発生させる

テストコード

```python:tests/test_script.py
import pytest
from src.script import sample1


def test_sample1_mock_sample2_exception(mocker):
    """
    sample2関数が呼び出されたときにExceptionを発生させる。
    """
    url = "https://hogehoge.com"
    mocker.patch("src.script.sample2", side_effect=Exception)
    with pytest.raises(Exception):
        sample1(url)
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py .                                         [100%] 

========================= 1 passed in 0.17s =========================
```

## モジュールの関数をMockする
sample2関数をテストするときに、requestモジュールのget関数を置き換える。

### モジュールの関数の返り値を指定する

テストコード

```python:tests/test_script.py
import requests
from src.script import sample2


def test_sample2_mock_requests_200(mocker):
    """
    sample2関数で実行されるrequests.getを置き換える。
    また、返り値もMockでstatus_codeが200で返るように置き換える。
    """
    status_code = 200
    url = "https://hogehoge.com"
    response_mock = mocker.Mock()
    response_mock.status_code = status_code
    mocker.patch.object(requests, "get", return_value=response_mock)
    assert sample2(url) == status_code


def test_sample2_mock_requests_404(mocker):
    """
    sample2関数で実行されるrequests.getを置き換える。
    また、返り値もMockでstatus_codeが404で返るように置き換える。
    """
    status_code = 404
    url = "https://fugafuga.com"
    response_mock = mocker.Mock()
    response_mock.status_code = status_code
    mocker.patch.object(requests, "get", return_value=response_mock)
    assert sample2(url) == status_code
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py ..                                        [100%] 

========================= 2 passed in 0.22s =========================
```

### モジュールの関数呼び出し時にExceptionを発生させる

テストコード

```python:tests/test_script.py
import requests
from src.script import sample2


def test_sample2_mock_request_exception(mocker):
    """
    sample2関数でrequests.getが呼び出されたときにExceptionを発生させる。
    """
    status_code = 0
    url = "https://hogehoge.com"
    mocker.patch.object(requests, "get", side_effect=Exception)
    assert sample2(url) == status_code
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py .                                         [100%] 

========================= 1 passed in 0.19s =========================
```

## クラスのメソッド内で呼ばれる関数をMockする
SampleClassクラスのgetメソッドをテストするときに、中で呼ばれる_reqのget関数を置き換える。

### クラスのメソッド内で呼ばれる関数の返り値を指定する

テストコード

```python:tests/test_script.py
from src.script import SampleClass


class TestSampleClass:
    def test_get_200(self, mocker):
        """
        SampleClassのgetメソッドで呼び出される_req.getを置き換える。
        また、返り値もMockでstatus_codeが200で返るように置き換える。
        """
        status_code = 200
        url = "https://hogehoge.com"
        sample = SampleClass()
        response_mock = mocker.Mock()
        response_mock.status_code = status_code
        req_mock = mocker.MagicMock()
        req_mock.get = mocker.Mock(return_value=response_mock)
        mocker.patch.object(sample, "_req", req_mock)
        assert sample.get(url) == status_code
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py .                                         [100%] 

========================= 1 passed in 0.21s =========================
```

### クラスのメソッド内で呼ばれる関数の呼び出し時にExceptionを発生させる

テストコード

```python:tests/test_script.py
from src.script import SampleClass


class TestSampleClass:
    def test_get_exception(self, mocker):
        """
        SampleClassのgetメソッドで_req.getが呼び出されたときにExceptionを発生させる。
        """
        status_code = 0
        url = "https://hogehoge.com"
        sample = SampleClass()
        req_mock = mocker.MagicMock()
        req_mock.get = mocker.Mock(side_effect=Exception)
        mocker.patch.object(sample, "_req", req_mock)
        assert sample.get(url) == status_code
```

実行結果

```bash
 > pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item

tests\test_script.py .                                         [100%] 

========================= 1 passed in 0.21s =========================
```

# まとめ
いろいろと調べて自分なりにやってみただけなので、正しい使い方なのかはわかりません。