🙌

Pythonのunittestでmockを使用する

2022/10/10に公開約4,000字

概要

  • Pythonのunittestで関数をモック化する方法です
  • モック化するにはunittest.mockのMagicMockもしくはpatchを使用します
  • APIの呼び出しなどでrequestsを使用しており、requestsをモックにする場合はrequests-mockというライブラリを使用できます

バージョン情報

  • Python: 3.10.7
  • requests-mock: 1.10.0

各ライブラリの公式サイト

サンプルコード
サンプルコードはこちらです。

対象のコード

今回の記事では外部APIの呼び出し(api.call_api)をモックにすることを想定します。

sample.py
import api

def main(url):
    response = api.call_api(url)
    return response.text
api.py
import requests

# この関数をモック化する
def call_api(url):
    response = requests.get(url)
    return response

MagicMockを使用する

関数をmockにする

関数をmockにするにはMagicMockを使用します。
assert_called_withを使用することでmockにした関数が呼ばれたかどうかを検証できます。

test_sample.py
class TestSample(unittest.TestCase):
    def test_mock_called(self):
        # api.call_apiをmockにする
        mock = MagicMock()
        api.call_api = mock
        
        url = "https://example.com"
        main(url)

        # call_apiが、"https://example.com"の引数で呼ばれたことを検証する
        mock.assert_called_with("https://example.com")

戻り値を設定する

mockの戻り値を設定する場合は、MagicMockのreturn_valueを設定します。

test_sample.py
class ResponseMock:
    def __init__(self, text):
        self.text = text

class TestSample(unittest.TestCase):
    def test_mock_return_value(self):
        # call_apiをモック化して、戻り値をResponseMock("mocked_text")に設定する
        mock = MagicMock(return_value=ResponseMock("mocked_text"))
        api.call_api = mock

        url = "https://example.com"
        result = main(url)

        self.assertEqual(result, "mocked_text")

戻り値を変える

関数の呼び出し方法によってmockの戻り値を変えたい場合は、MagicMockのside_effectを設定します。
argsには関数を呼び出したときの引数が入っているため、以下の例ではargsの値によって戻り値を変えています。

test_sample.py
class TestSample(unittest.TestCase):
    def test_mock_side_effect(self):
        # argsにはcall_apiを呼び出したときの引数が入る
        # 引数によって戻り値を変える
        def mock_return_value(*args, **kwargs):
            if args[0] == "https://example.com":
                result = "mocked_text"
            else:
                result = "not_found_text"
            return ResponseMock(result)
        mock = MagicMock(side_effect=mock_return_value)
        api.call_api = mock

        # test1
        url = "https://example.com"
        result = main(url)

        self.assertEqual(result, "mocked_text")

        # test2
        url = "https://notfound.com"
        result = main(url)

        self.assertEqual(result, "not_found_text")

patchを使う

特定のテストを実行しているときだけmockにしたいことがあります。
そういった場合はpatchを使用します。

context managerとして使用する

patchをcontext managerとして使用する方法です。

test_sample.py
class TestSamplePatch(unittest.TestCase):
    def test_mock_patch(self):
        mock = MagicMock(return_value=ResponseMock("mocked_text"))
        with patch('api.call_api', mock):
            print(api.call_api)
            # output: <MagicMock id='xxxxxxxxxx'>
            url = "https://example.com"
            result = main(url)

            self.assertEqual(result, "mocked_text")

        print(api.call_api)
        # output: <function call_api at xxxxxxxxx>

decoratorを使用する

また、patchはdecoratorとしても使用できます。

test_sample.py
class TestSamplePatch(unittest.TestCase):

    mock = MagicMock(return_value=ResponseMock("mocked_text"))
    @patch('api.call_api', mock)
    def test_mock_patch_decorator(self):
        url = "https://example.com"
        result = main(url)

        self.assertEqual(result, "mocked_text")

requests-mockを使用する

外部APIの呼び出しにrequestsを使用している場合はrequests-mockを使用すれば簡単にrequestsをmock化することができます。

test_sample.py
class TestSampleRequestMock(unittest.TestCase):
    def test_mock_request(self):
        url = "https://example.com"
        with requests_mock.Mocker() as mock:
            mock.get(url, text="mocked_text")

            result = main(url)
            
            self.assertEqual(result, "mocked_text")
GitHubで編集を提案

Discussion

ログインするとコメントできます