はじめに
pytestを使用してテストコードを書いたときに、Mockの使い方に苦労したのでまとめてみました。
Mockとは
Mockは特定のオブジェクトの代理をして、ユニットテストを円滑に進めるためのモジュールです。
準備
-
pytestのインストール
pip install pytest pytest-mock
-
テスト対象のコードの作成
ディレクトリ構成
root ├─src │ └─script.py ・・・テスト対象のソースコード └─tests ├─__init__.py ・・・空ファイル └─script.py ・・・テストコード
テスト対象のソースコード
src/script.pyimport requests def sample1(url): return sample2(url) def sample2(url): try: response = requests.get(url) return response.status_code except Exception: return 0 class SampleClass: def __init__(self): self._req = requests def get(self, url): try: response = self._req.get(url) return response.status_code except Exception: return 0
Mockを使用したテストの実施
関数をMockする
sample1関数をテストするときに、sample2関数を置き換える。
関数の返り値を指定する
テストコード
tests/test_script.py
from src.script import sample1
def test_sample1_mock_sample2_200(mocker):
"""
sample2関数が200を返すようする。
"""
status_code = 200
url = "https://hogehoge.com"
mocker.patch("src.script.sample2", return_value=status_code)
assert sample1(url) == status_code
def test_sample1_mock_sample2_404(mocker):
"""
sample2関数が404を返すようする。
"""
status_code = 404
url = "https://fugafuga.com"
mocker.patch("src.script.sample2", return_value=status_code)
assert sample1(url) == status_code
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 2 items
tests\test_script.py .. [100%]
========================= 2 passed in 0.19s =========================
関数の返り値を動的に変更する
テストコード
tests/test_script.py
from src.script import sample1
def test_sample1_mock_sample2(mocker):
"""
sample2関数がurlによって200,404,0のいずれかを返すようする。
"""
def return_status_code(url):
"""
urlによって200,404,0のいずれかを返す
"""
if url == "https://hogehoge.com":
return 200
elif url == "https://fugafuga.com":
return 404
else:
return 0
mocker.patch("src.script.sample2", side_effect=return_status_code)
assert sample1("https://hogehoge.com") == 200
assert sample1("https://fugafuga.com") == 404
assert sample1("https://higehige.com") == 0
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py . [100%]
========================= 1 passed in 0.20s =========================
関数呼び出し時にExceptionを発生させる
テストコード
tests/test_script.py
import pytest
from src.script import sample1
def test_sample1_mock_sample2_exception(mocker):
"""
sample2関数が呼び出されたときにExceptionを発生させる。
"""
url = "https://hogehoge.com"
mocker.patch("src.script.sample2", side_effect=Exception)
with pytest.raises(Exception):
sample1(url)
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py . [100%]
========================= 1 passed in 0.17s =========================
モジュールの関数をMockする
sample2関数をテストするときに、requestモジュールのget関数を置き換える。
モジュールの関数の返り値を指定する
テストコード
tests/test_script.py
import requests
from src.script import sample2
def test_sample2_mock_requests_200(mocker):
"""
sample2関数で実行されるrequests.getを置き換える。
また、返り値もMockでstatus_codeが200で返るように置き換える。
"""
status_code = 200
url = "https://hogehoge.com"
response_mock = mocker.Mock()
response_mock.status_code = status_code
mocker.patch.object(requests, "get", return_value=response_mock)
assert sample2(url) == status_code
def test_sample2_mock_requests_404(mocker):
"""
sample2関数で実行されるrequests.getを置き換える。
また、返り値もMockでstatus_codeが404で返るように置き換える。
"""
status_code = 404
url = "https://fugafuga.com"
response_mock = mocker.Mock()
response_mock.status_code = status_code
mocker.patch.object(requests, "get", return_value=response_mock)
assert sample2(url) == status_code
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py .. [100%]
========================= 2 passed in 0.22s =========================
モジュールの関数呼び出し時にExceptionを発生させる
テストコード
tests/test_script.py
import requests
from src.script import sample2
def test_sample2_mock_request_exception(mocker):
"""
sample2関数でrequests.getが呼び出されたときにExceptionを発生させる。
"""
status_code = 0
url = "https://hogehoge.com"
mocker.patch.object(requests, "get", side_effect=Exception)
assert sample2(url) == status_code
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py . [100%]
========================= 1 passed in 0.19s =========================
クラスのメソッド内で呼ばれる関数をMockする
SampleClassクラスのgetメソッドをテストするときに、中で呼ばれる_reqのget関数を置き換える。
クラスのメソッド内で呼ばれる関数の返り値を指定する
テストコード
tests/test_script.py
from src.script import SampleClass
class TestSampleClass:
def test_get_200(self, mocker):
"""
SampleClassのgetメソッドで呼び出される_req.getを置き換える。
また、返り値もMockでstatus_codeが200で返るように置き換える。
"""
status_code = 200
url = "https://hogehoge.com"
sample = SampleClass()
response_mock = mocker.Mock()
response_mock.status_code = status_code
req_mock = mocker.MagicMock()
req_mock.get = mocker.Mock(return_value=response_mock)
mocker.patch.object(sample, "_req", req_mock)
assert sample.get(url) == status_code
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py . [100%]
========================= 1 passed in 0.21s =========================
クラスのメソッド内で呼ばれる関数の呼び出し時にExceptionを発生させる
テストコード
tests/test_script.py
from src.script import SampleClass
class TestSampleClass:
def test_get_exception(self, mocker):
"""
SampleClassのgetメソッドで_req.getが呼び出されたときにExceptionを発生させる。
"""
status_code = 0
url = "https://hogehoge.com"
sample = SampleClass()
req_mock = mocker.MagicMock()
req_mock.get = mocker.Mock(side_effect=Exception)
mocker.patch.object(sample, "_req", req_mock)
assert sample.get(url) == status_code
実行結果
> pytest
======================== test session starts ========================
platform win32 -- Python 3.8.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: C:\root
plugins: cov-2.10.0, mock-3.3.1
collected 1 item
tests\test_script.py . [100%]
========================= 1 passed in 0.21s =========================
まとめ
いろいろと調べて自分なりにやってみただけなので、正しい使い方なのかはわかりません。