17
16

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 3 years have passed since last update.

pytestでMockを使う

Posted at

はじめに

pytestを使用してテストコードを書いたときに、Mockの使い方に苦労したのでまとめてみました。

Mockとは

Mockは特定のオブジェクトの代理をして、ユニットテストを円滑に進めるためのモジュールです。

準備

  1. pytestのインストール

    pip install pytest pytest-mock
    
  2. テスト対象のコードの作成

    ディレクトリ構成

    root
    ├─src
    │   └─script.py    ・・・テスト対象のソースコード
    └─tests
        ├─__init__.py  ・・・空ファイル
        └─script.py    ・・・テストコード
    

    テスト対象のソースコード

    src/script.py
    import 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 =========================

まとめ

いろいろと調べて自分なりにやってみただけなので、正しい使い方なのかはわかりません。

17
16
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
17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?