最近Python3 + unittestでテストをよく書いています。
unittest.mock
の使い方がいまいちわかっていなかったのですが、テストを書いていくうちにいくつかのパターンがわかってきたのでまとめてみました。
環境
サンプルコードは以下の環境で実行しています。
$ python3 --version
Python 3.6.3
HTTPリクエストの結果によって変わる振る舞いをテスト
以下は外部のAPIサーバの死活チェックをする関数です。
例外(Timeout
, ConnectionError
)もしくはステータスコードが200以外の場合はFalse
、ステータスコードが200の場合はTrue
を返します。
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
を利用してダミーオブジェクトを作りました。
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のインスタンスを停止するプログラムです。
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
に設定して呼び出されたかをテストするようになっています。
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']
)
まとめ
テストを書くことが目的化してしまうと意味がないので、この関数では何をテストすればよいのか、ということを意識してテストを書くことが重要だと思いました。
間違いなどありましたらコメントいただけると幸いです。