31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-02

最近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_responserequests.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']
        )

まとめ

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

31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?