私が歌川です

@utgwkk が書いている

Pythonのテストで常にHTTPリクエストをモックする

HTTPリクエストをモックするには

PythonでHTTPリクエストをモックするには、どうしたらいいか。unittest.mockを使ってHTTPリクエストを投げるメソッドをモックする方法が思い付くかもしれない。しかし、どこをモックして何を返すようにしたらよいのかは自明ではない。

HTTPrettyというライブラリを使うと、簡単にHTTPリクエストをモックできる。どんなライブラリを使っているのか、インタフェースがどうなのかを気にする必要がない。

テスト中は常にモックする

PerlのTest::WWW::Stubに近いことをPythonでも実現したい。つまり、テスト中は常にHTTPリクエストをモックして外部にリクエストが飛ばないようにしたい。

pytestを使っているなら、以下のようなfixtureをconftest.pyに書くのが手軽だと思う。

import httpretty
import pytest


@pytest.fixture(scope="function", autouse=True)
def mock_http_request():
    with httpretty.enabled(allow_net_connect=False):
        yield

こうすると、外部にHTTPリクエストを飛ばしたら例外が送出されてテストが落ちるようになる。

Python標準のunittestでは試してないけど、TestCaseクラスを継承して、HTTPリクエストをモックするsetUpメソッド (片付けるtearDownメソッド) を実装すれば実現できるのではないか。

特定のリクエストに対してダミーレスポンスを返す

特定のURLへのリクエストにダミーのレスポンスを返したいなら、 yield の前で httpretty.register_uri 関数を呼び出すようにしたらいいし、テストメソッド内で呼ぶようにしてもよさそう。ダミーレスポンスを適当にファイルに切り出しておいたら取り回しがいいと思う。

以下の例では、Slack APIの呼び出しをモックしてダミーのレスポンスを返すように差し替えている。

from pathlib import Path
import httpretty
import pytest


@pytest.fixture(scope="function", autouse=True)
def mock_http_request():
    with httpretty.enabled(allow_net_connect=False):
        body = (
            Path(__file__).parent / "data/httpmock/slack_chat_postMessage.json"
        ).read_text()
        httpretty.register_uri(
            httpretty.POST, re.compile(r"^https://(www[.])?slack[.]com/api/chat[.]postMessage$"), body=body
        )
        yield

他の方法との比較

テストメソッドごとにHTTPリクエストをモックするかどうか決めたいならpytest-httprettyというプラグインが便利そう。httpretty というマーカーをテストに設定したらHTTPリクエストがモックされるようになる。

個人的には、常にHTTPリクエストがモックされている方が、テストの再現性や本番のAPIを叩いてしまうことを回避する側面などを考えると、よりよいと思ったので自分で仕組みを用意した。モックしたいリクエストだけモックするのではなく、常に外部リクエストが飛ばないようにして必要に応じて開放する方が安心できると思う。