pytestのmockついて調べた話

ちょっと使い方を調べた内容です。
pytest-mockの仕様まで踏み込んでいません。
公式のGithubのREADME見ただけでもっと機能があるからちゃんと調べないといけないと思ってます…。

Contents

pytestとは

Pythonでテストコードを書く際に利用するテストモジュールです。
組み込みのunittestもありますが、テストに失敗したときの結果の見やすさが人気です。
詳しい話は多くの方が記事を書かれているので、探してみてください。

本記事では、私がpytestでmockの使い方を調べていた時に2通りの書き方を見つけたので、それぞれどのように動くのかを調べました。

pytest-mock

今回使用する単体テストモジュールです。

https://github.com/pytest-dev/pytest-mock/

pip install pytest-mockでインストール可能です。本記事の内容ではimport文は不要です。

今回のmockの想定利用シーン

想定するメソッドと実施したいテストは以下の条件とします。

  1. 指定されたURLにHTTPリクエスト(今回はGET)をしてコンテンツを呼び出し元に返すメソッド
  2. このメソッドはstatus codeに応じて例外を発生させる
  3. status codeが200以外の場合(404や500)のテストを行いたい
  4. 実際にHTTP通信をしなくてもテストを行いたいケース

「正常系(200)は問題ないけど、異常系(404,500など)のテストをしたい。」です。

実施環境

OS: Windows10 Home 64bit
  WSL2 Ubuntu20.04
Python: 3.6.8 (pyenv, pipenv使用)
pytest-6.1.1
mock-3.3.1

色々省いていますが、ディレクトリ構成。

$ tree
.
├ ─ ─  src
│    ├ ─ ─  OreoreRequests.py
└ ─ ─  test
    ├ ─ ─  test_OreoreRequests.py

リクエスト先として、リクエストされたことがすぐにわかるようにWebサーバを立ち上げておきます。以下で8000番で立ち上がります。

python -m http.server

テスト対象コード

import requests


class OreoreRequests:
    def __init__(self, url):
        self.url = url

    def get(self):
        print("DEBUGDEBUGDEBUG")
        res = requests.get(self.url)
        if res.status_code != 200:
            raise Exception("status code is not 200.")

        result = {
                "statuscode": res.status_code,
                "text": res.text
                }
        return result

簡単のためにメソッドの中に入ったかどうかを確認するためのprint文を入れています。後でmockを使ったときにメソッドに入ったかどうかを確認するための記述です。
HTTPリクエストを行って、HTTPレスポンスのステータスコードに応じて例外を発生させます。
ちゃんと書くときは独自例外クラスを使うなどして適切な例外を発生させてくださいね。

上記メソッドではHTTPレスポンスのステータスコードが200以外だったら例外を発生させます。ガバガバですねw
メソッドの戻り値はステータスコードとbodyを辞書形式で返しています。ステータスコードいる?

テストコード

分割して書きます。
import文はこんな感じになりました。

import pytest
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from src.OreoreRequests import OreoreRequests

テスト実行時のコマンド

実際のテストコードを紹介する前に実行コマンドを記載しておきます。これから先の実行は全てこのコマンドで行います。
以下のコマンドで実行します。-sオプションで標準出力をコンソールに表示します。

pytest test/test_OreoreRequests.py -v -s
# pipenv 使用時はこちら。
pipenv run pytest test/test_OreoreRequests.py -v -s

正常系テスト

class TestOreoreRequests:

    def test_get_200(self):
        ore = OreoreRequests("http://localhost:8000/")
        result = ore.get()
        assert result["statuscode"] == 200

テストを実行したコンソールにPASSEDと出ると思います。
また起動したHTTPサーバのログに"GET / HTTP/1.1" 200 -と出ているかと思います。
想定通りWebサーバにアクセスしていることがわかります。

mock使用の異常系テストその1

以下のmockの書き方では、requests.get()をmockにしています。
メソッド全体ではなくrequets.get()だけをmockにしています。
テスト内容は、404だったらExceptionが発生することを期待しています。
また、HTTPリクエストをmockにしたので、Webサーバにリクエストが飛ばないことも期待しています。これはコンソールで確認します。

class TestOreoreRequests:
    def test_get_404(self, mocker):
        ore = OreoreRequests("http://localhost:8000/")

        mock = mocker.Mock()
        mock.status_code = 404
        mock.text = "Not Found"
        mocker.patch("requests.get").return_value = mock

        with pytest.raises(Exception):
            ore.get()

このテストを実行したコンソールにPASSEDと出ます。つまりちゃんと例外が発生しています。
確認のためにwith句のExceptionValueErrorなどと置き換えて再度テストを実行すると、この箇所で失敗するのがわかります。
コンソールを確認するとリクエストを受け付けていないことも確認できます。
HTTPリクエストを行う処理だけがmockに置き換えられています。想定通りの動きですね。

mock使用の異常系テストその2

以下の書き方では、OreoreRequestsクラスのgetメソッドをmockにしています。メソッドそのものです。つまりOreoreRequests.getメソッドの中は通りません。
テストを実行しても標準出力に`DEBUGDEBUGDEBUG`は表示されません。

本来なら以下のテストは失敗します。が、with句でExceptionを期待しているため成功します。
コメントにも書きましたがwith句の中の`ore.get()`はTypeErrorを発生させます。

class TestOreoreRequests:
    def test_get_500(self, mocker):
        ore = OreoreRequests("http://localhost:8000/")
        result_mock = {
                "statuscode": 500,
                "text": "Internal Server Error"
                }
        mocker.patch.object(
                OreoreRequests,
                "get",
                result_mock
                )
        with pytest.raises(Exception):
            result = ore.get()    # TypeError: 'dict' object is not callable
        result = ore.get
        assert result["statuscode"] == 500  # # test passed.
        print(ore.get is result_mock)

実行結果から、
mocker.patch.objectメソッドは、指定したクラスのメソッド(関数オブジェクト)をresult_mockというdictオブジェクトに置き換える
と、考えます。(仕様として正しいかどうかは要調査です。)

この考察を確認するために文末のprint文でore.getresult_mockが同じオブジェクトかどうかを確認する文を入れたところ、テストの実行結果にはTrueが出力されました。
コンソールを確認するとリクエストを受け付けていないことも確認できます。

まとめ

今回のテストケースは、HTTPステータスコードによる処理の確認なので、mock使用の異常系テストその1 の使用が妥当だと思われます。

今回のケースでは mock使用の異常系テストその2 は目的と一致しませんでしたが、スタブとしては使えそうですね。(スタブとモックとドライバーと、ちゃんとした言葉の定義がいつも覚えられない。)

シェアする

  • このエントリーをはてなブックマークに追加

フォローする