python mockでテスト 使用例まとめ 置換、複数回置換、呼出回数チェック等

python mockでテスト 使用例まとめ 置換、複数回置換、呼出回数チェック等
pythonで試験をするときに、
ダミーの値を設定したい場合が出ます。
その時にmockを利用すると、異常系試験でもスムーズに
テストソースを記述することができます。

ただ、私がよく使用するmockの使用方法が
検索してもひとまとめになっていなく探すのに手間がかかったり、
テストソースを具体的に記述するときの
最初のとっかかりが若干わかりにくかったりしたので、
mockを使用したテストの使用方法を
具体的なテストソースを使用例にしてまとめたいと思います。

使用例としては複数回実行、呼出回数チェック等があります。

記述した例以外にも色々な使用方法がありますので、
さらなる情報は公式ページ
https://docs.python.org/ja/3/library/unittest.mock-examples.html
を参照ください。

また、検索すると、MagicMockとmockの記述が混ざって
出てくるため、これらの違いについても記述します。
当ページでは 公式ページで推奨している。
MagicMock を利用したソースを記述します。

mockの基本的な使用方法について

mockを利用する基本的な記述方法は
[変更したいメソッド] = [mockオブジェクト]
となります。

例えばjsonのdumpsの返却値を変更したい場合、
下記のとおり、呼び出すメソッドjson.dumpsに = でmockオブジェクトを
設定します。
これによって、以降呼びされた場合は、
設定したMockオブジェクトの return_value に設定した値が
返却されることになります。
from unittest.mock import MagicMock
import json

json.dumps = MagicMock(return_value={"key" : "data"})

# パラメータに何を渡しても設定した値。ここでは{"key" : "data"}が返却される。
print(json.dumps("aaa"))
これが一番基本的な使用方法で、
これ以外にテストのパターンによって、必要な記述をすることになります。

テスト対象ソース(以降のテストソースの呼出と検証で使用)

テスト対象のソースを以下の通り記述します。
以降、このテスト対象ソースを試験するためのパターンを例にして
モックの説明をしたいと思います。

対象となるテストソースは
郵便番号取得APIに対してリクエストを発行し、
取得したJsonデータをリストに格納し返却するというものです。
import requests


DEF_ZIP_CODES = ["1100001", "1800002"]


class ZipCodeUtil:
    """
    郵便番号関連の便利クラス
    """
    def get_zip_codes(self):
        """
        郵便番号詳細取得メソッド。
        定数の番号から郵便番号検索APIを利用して詳細を取得する。
        :return: 郵便番号詳細リスト
        """
        ret_list = []
        for zip_code in DEF_ZIP_CODES:
            url = f"https://zip-cloud.appspot.com/api/search?zipcode={zip_code}"
            response = requests.get(url)
            if response.status_code > 300:
                print(f"error:{response.status_code}")
                return

            ret_list.append(response.json())

        # レスポンスをすべて返却
        return ret_list

    def get_def_str(self):
        return DEF_ZIP_CODES

使用例:単純にメソッドの返却値を変える 場合

テストソース

例として試験するソースは以下の通り。
単純に変数を返却する箇所を修正したものと(test_02_使用例単純な値を返却)、
変数にクラスを設定し、より柔軟に対応できるようにしたものです(test_03
使用例クラスを返却)。

ちなみにtest_01
正常系の前にモックをすると、
モックの値でtest_01_正常系が動作するため、異常になります。
(ここでは01,02,03の番号でunittestの実行順序を制御)
そうならないためのモック範囲指定については、
使用例: スコープを指定してモックを利用
を参照ください。
import requests
import unittest
from unittest.mock import MagicMock
import mail_requests
import json

class MyTestCase(unittest.TestCase):

    def test_01_正常系(self):
        zip_util = mail_requests.ZipCodeUtil()
        zip_codes = zip_util.get_zip_codes()
        for zip_code in zip_codes:
            print(zip_code["status"])
            self.assertEqual(200, zip_code["status"])

    def test_02_使用例_単純な値を返却(self):
        mail_requests.ZipCodeUtil.get_def_str = MagicMock(return_value="mock_test")
        zip_util = mail_requests.ZipCodeUtil()
        zip_codes = zip_util.get_def_str()

        self.assertEqual("mock_test", zip_codes)

    def test_03_使用例_クラスを返却(self):
        requests.get = MagicMock(return_value =HttpResponse('{"status" : 400}', 200))
        zip_util = mail_requests.ZipCodeUtil()
        zip_codes = zip_util.get_zip_codes()
        for zip_code in zip_codes:
            print(zip_code["status"])
            self.assertEqual(400, zip_code["status"])

class HttpResponse:
    def __init__(self, json_data, status_code):
        # get_zip_codesで使用する値のみ設定。
        # 他にもjson()を使用する場合等はそのメソッドを追記する。
        self.text = json_data
        self.status_code = status_code

    def json(self):
        return json.loads(self.text)

説明

テストメソッドtest_02_使用例_単純な値を返却では、
ZipCodeUtilのget_def_strという、テスト対象となるメソッドを
MagicMockで”mock_test”という文字列に置き換えています。
置き換え後、self.assertEqual(“mock_test”, zip_codes)で
値のチェックをすると、置き換えた値が
返却され、assertの結果がOKとなります。

もう少し複雑なことをしたい場合の例として、
test_03_使用例_クラスを返却を見てみると、
requests.getを自作したレスポンス用のクラス(HttpResponse)に
モックで置き換えています。
これをテスト対象で見ると、
response = requests.get(url)
の response の箇所にモックオブジェクトが入ることになります。
response は後続で
if response.status_code > 300:
ret_list.append(response.json())
使用されているため、そこでこけないような
モックオブジェクトである必要があります。
このようなパターンの場合 HttpResponse のように自前でクラスを作成して
設定することで対応可能となります。

このようにrequestsのようなライブラリでは返却した値は
通常、テスト対象のソースで記述したように
ステータスコードの確認やレスポンスのボディ部を取得するため、
status_code の属性やjson()のようなメソッドを使用します。
それらをモックするためにはHttpResponseのような
呼び出されるものをカバーしたクラス等が必要となります。

使用例: メソッドの返却値を呼出回数に応じて変える

テストソース

例として試験するソースは以下の通り。
side_effectという属性を利用して、
回数に応じた値を返却することが可能となります。
import requests
import unittest
from unittest.mock import MagicMock
import mail_requests
import json

class MyTestCase(unittest.TestCase):

    def test_回数指定モック(self):
        mock_list = [HttpResponse('{"status" : 200}', 200),HttpResponse('{"status" : 400}', 200)]
        requests.get = MagicMock(side_effect=mock_list)
        zip_util = mail_requests.ZipCodeUtil()
        zip_codes = zip_util.get_zip_codes()

        self.assertEqual(200, zip_codes[0]["status"])
        self.assertEqual(400, zip_codes[1]["status"])

class HttpResponse:
    def __init__(self, json_data, status_code):
        # get_zip_codesで使用する値のみ設定。
        # 他にもjson()を使用する場合等はそのメソッドを追記する。
        self.text = json_data
        self.status_code = status_code

    def json(self):
        return json.loads(self.text)

説明

モックの引数がside_effect=となり、
こちらに値をリスト形式で設定できます。
これで設定したリストの値の順にモックの値が返却されることになります。
side_effect はリスト以外にも例外を設定することも可能です。

今回はダミーデータを設定しましたが、
reqeusts.getの正常なデータをあらかじめ実行して取得し、
それを設定することで、ダミーではないデータを途中で
差し込むことも可能です。

使用例: スコープを指定してモックを利用

テストソース

使用範囲を限定して、利用する場合は以下の通り。
これはある場合はモックを設定して、
ある場合はモックを設定したくない場合等で使用します。
例えば、def test_スコープ指定モック(self):
の次にテストケースを書く場合、スコープを決めないと、
仮に def test_スコープ指定モックその2(self):
のような次のテストを設定するとそこでは、
requests.getが常にモックになってしまいます。
そのような場合を防ぐ時等に効果を発揮します。
import requests
import unittest
from unittest.mock import patch
import mail_requests
import json

class MyTestCase(unittest.TestCase):

    def test_スコープ指定モック(self):
        mock_list = [HttpResponse('{"status" : 500}', 200),HttpResponse('{"status" : 400}', 200)]

        with patch('requests.get') as mock_date:
            mock_date.side_effect = mock_list
            zip_util = mail_requests.ZipCodeUtil()
            zip_codes = zip_util.get_zip_codes()

            self.assertEqual(500, zip_codes[0]["status"])
            self.assertEqual(400, zip_codes[1]["status"])

        # withの範囲外なので、こちらはモックしていないrequests.getが使用可能
        zip_util = mail_requests.ZipCodeUtil()
        zip_codes = zip_util.get_zip_codes()

        self.assertEqual(200, zip_codes[0]["status"])
        self.assertEqual(200, zip_codes[1]["status"])

class HttpResponse:
    def __init__(self, json_data, status_code):
        # get_zip_codesで使用する値のみ設定。
        # 他にもjson()を使用する場合等はそのメソッドを追記する。
        self.text = json_data
        self.status_code = status_code

    def json(self):
        return json.loads(self.text)

説明

with patch('requests.get') as mock_date:の箇所で、
モックがかかるスコープをwithのインデント配下で
限定します。
as mock_date のmock_dataにMagicMockが入るので、
そこの属性を設定することで、モックの値の設定が可能です。

withのほかに1メソッド全体にスコープを充てる方法として
@patchもあります。
使い方はdefの上に@patchを設定する以外には
特に今までと使用方法は変わりません。
例は下記のとおりです。
~~途中省略~~
from unittest.mock import patch, MagicMock

    @patch('requests.get', MagicMock(side_effect=[HttpResponse('{"status" : 500}', 200), HttpResponse('{"status" : 400}', 200)]))
    def test_回数指定モック(self):
~~省略~~

使用例: 呼び出した回数の確認

テストソース

テスト対象メソッドget_zip_codes
郵便番号コードのリストDEF_ZIP_CODESに2つ番号を設定しているため、
2回requests.getが呼び出しされる想定です。
ここでは2回呼び出しがされているか、
想定の順番で呼び出しがされているかを確認しています。
import requests
import unittest
from unittest.mock import MagicMock, call
import mail_requests
import json

class HttpResponse:
    def __init__(self, json_data, status_code):
        # get_zip_codesで使用する値のみ設定。
        # 他にもjson()を使用する場合等はそのメソッドを追記する。
        self.text = json_data
        self.status_code = status_code

    def json(self):
        return json.loads(self.text)

class MyTestCase(unittest.TestCase):
    def test_回数指定モック(self):
        mock = MagicMock(return_value=HttpResponse('{"status" : 200}', 200))
        requests.get = mock

        zip_util = mail_requests.ZipCodeUtil()
        zip_util.get_zip_codes()

        print(f"何回呼ばれたか確認:{mock.call_count}")
        self.assertEqual(2, mock.call_count)

        # 呼び出しURL
        url_1 = f"https://zip-cloud.appspot.com/api/search?zipcode=1100001"
        url_2 = f"https://zip-cloud.appspot.com/api/search?zipcode=1800002"

        # 最後に呼び出しされた形式を確認
        mock.assert_called_with(url_2)  # これはOK。

        # 指定したURLで1回以上呼び出しされているか
        mock.assert_any_call(url_1)
        mock.assert_any_call(url_2)

        # リストの順番で呼び出しがあったか。callメソッドも使用する。
        calls = [call(url_1), call(url_2)]
        mock.assert_has_calls(calls)

説明

呼び出し回数を確認するためには、
検証対象をMagicMockでモック化する必要があります。

検証対象のメソッドに設定したモックに設定されている、
呼び出し検証用のメソッドを使用することで、
何回呼び出しをしているかの検証が可能となります。

検証用のメソッドは以下があります。
・呼び出しがあったことを確認:assert_called
・最後に呼び出しされた形式を確認:assert_called_with
・指定したパラメータで1回以上呼び出しがあるか確認:assert_any_call
・想定している順序で呼び出しがあるか確認:assert_has_calls
・1回も呼び出しが無いことを確認: assert_not_called

補足:mockとMagicMockの違い

mockでもテストで問題なく使用できるのですが、
MagicMockとの違いは特殊メソッドをデフォルトで実装している点です。
具体的にはhttps://qiita.com/kanae_y/items/4019fbfe4db4a07521cd
のページがよくまとまっているため、こちらを参照ください。

公式では機能が豊富なMagicMockの方を
使いましょうと記述されており、
使ってみると確かに色々便利であったり、
patchで入ってくるデータは MagicMock だったりするので、
MagicMockを利用することを推奨します。

終わりに

mockは、
連携先が存在しない環境下での試験や、
異常系の試験であってもテストソースとして組み込むことが可能となるため、
細かな単体テストソースを作成するには必要な機能です。

今後も公式ページやいろいろな方のページを参考にしたり、
他にも使えそうなパターンを記述してみたりして
使い方を勉強していきたいと思います。

この記事が 少しでもmockの使用の助けになれれば幸いです。

ITカテゴリの最新記事