Python3 + unittest + unittest.mock を使ったユニットテスト

最近Python3 + unittestでテストをよく書いています。
`unittest.mock`の使い方がいまいちわかっていなかったのですが、テストを書いていくうちにいくつかのパターンがわかってきたのでまとめてみました。

## 環境

サンプルコードは以下の環境で実行しています。

```
$ python3 --version
Python 3.6.3
```

## HTTPリクエストの結果によって変わる振る舞いをテスト

以下は外部のAPIサーバの死活チェックをする関数です。
例外(`Timeout`, `ConnectionError`)もしくはステータスコードが200以外の場合は`False`、ステータスコードが200の場合は`True`を返します。

```example.py
import requests

def is_alive_api():
    try:
        response = requests.get('https://example.com/api', timeout=10)
    except Timeout:
        return False
    except ConnectionError:
        return False

    if response.status_code != 200:
        return False
    return True
```

この関数でテストしたいのは、`requests.get`の結果によってそれぞれ意図した戻り値(`True` or `False`)が返ってくるかです。
ただし、`requests.get`の結果は接続先である https://example.com/api に依存しているので、この依存を切り離す必要がありそうです。

`test_is_alive_api1`を例に説明します。

まず`request.get`で任意の戻り値を返せるようにするために、`requests`をモックに置き換えます。
`unittest.mock.patch`をデコレーターで利用して、`'example.requests'`を引数に指定することでexapmle.pyでインポートした`requests`をモックに置き換えています。また、置き換えたモックは`test_is_alive_api1`の引数`mock_requests`で受け取ってテストケースの関数内で使えるようにします。

`mock_requests.get.return_value = mock_response` で`requests.get`の戻り値をダミーのレスポンス置き換えています。
ダミーレスポンスは`mock_response = Mock(status_code=200)`で定義しています。`unittest.mock.Mock`を利用してダミーオブジェクトを作りました。

```test_example.py
import unittest
from unittest.mock import Mock, patch

from requests import Timeout

import example


class TestExample(unittest.TestCase):

    @patch('example.requests')
    def test_is_alive_api1(self, mock_requests):
        mock_response = Mock(status_code=200)
        mock_requests.get.return_value = mock_response
        self.assertTrue(example.is_alive_api())

    @patch('example.requests')
    def test_is_alive_api2(self, mock_requests):
        mock_response = Mock(status_code=500)
        mock_requests.get.return_value = mock_response
        self.assertFalse(example.is_alive_api())

    @patch('example.requests')
    def test_is_alive_api3(self, mock_requests):
        mock_requests.get.side_effect = Timeout('Dummy Exception')
        self.assertFalse(example.is_alive_api())
```

## 特定の処理の呼び出し方法をテスト

boto3を使って指定したインスタンスIDのインスタンスを停止するプログラムです。

```example.py
import boto3


client = boto3.client('ec2')

def stop_instance(instance_id):
    client.stop_instances(
        InstanceIds=[instance_id]
    )
```

この関数では、意図どおりのインスタンスが停止されることを確認できればOKです。

テストに落とし込む方法は色々あると思いますが、ここでは`client.stop_instances`の引数に正しくインスタンスIDが設定されているかを確認するというアプローチのテストを書きます。

`client.stop_instances`の引数をテストするために、example.pyの`client`をモックに置き換えます。
その後、`example.stop_instance('dummy')`でテスト対象関数を実行した後に`client.stop_instances`がどのように呼び出されたかをテストします。

これには`mock_client.stop_instances.assert_called_once_with`を利用しています。
以下では`'dummy'`という文字列を`InstanceIds`に設定して呼び出されたかをテストするようになっています。

```test_example.py
import unittest
from unittest.mock import Mock, patch

import example


class TestExample(unittest.TestCase):
    @patch('example.client')
    def test_is_alive_api1(self, mock_client):
        example.stop_instance('dummy')
        mock_client.stop_instances.assert_called_once_with(
            InstanceIds=['dummy']
        )
```

## まとめ

テストを書くことが目的化してしまうと意味がないので、この関数では何をテストすればよいのか、ということを意識してテストを書くことが重要だと思いました。
間違いなどありましたらコメントいただけると幸いです。