nikkie-ftnextの日記

イベントレポートや読書メモを発信

HTTPXYZ は pytest と RESPX と一緒に使えます

~PyCon USでアメリカにいる期間は日本時間で1日のはじめにブログを公開し、そのあと更新する運用とします。 公開通知で見に来ていただいた方は、後ほど更新版をご確認ください(1日前の記事は更新されていて読めると思います)


はじめに

JST 5/15(金)の分です。nikkie (UUID 28fb3f96-a221-462c-93bd-567b431715b9) です。

メンテ停滞気味な HTTPX を friendly に forkした、HTTPXYZ。
推しの HTTP クライアントですが、HTTPX と同じように使えるのか気になって調べました。
今回はテストコードについてです。

目次

HTTPX をモックする RESPX

HTTP クライアント HTTPX のモックライブラリの1つが RESPX。
Quickstart を元にした例です。
tests/test_example.py

import httpx
import respx


@respx.mock(assert_all_called=True, assert_all_mocked=True)
def test_default(respx_mock):
    respx_mock.get("https://foo.bar/").mock(return_value=httpx.Response(204))
    response = httpx.get("https://foo.bar/")
    assert response.status_code == 204

リクエストのヘッダやデータの assert もできます。

HTTPXYZ もまた RESPX でモックできる!

ドキュメントに記載がありました。
Using respx in pytest (HTTPX Compatibility)

$ pytest -p httpxyz

試しに httpx -> httpxyz と変更します

import httpxyz
import respx


@respx.mock(assert_all_called=True, assert_all_mocked=True)
def test_default(respx_mock):
    respx_mock.get("https://foo.bar/").mock(return_value=httpxyz.Response(204))
    response = httpxyz.get("https://foo.bar/")
    assert response.status_code == 204

pytest -p httpxyzでテストは引き続き通ります!
pytesthttpxyzプラグインを early load するよう指定することで、RESPX で HTTPXYZ もモックできます

仕組み

httpxyz の pytest プラグインの実装のhttpxyz/_pytest_plugin.py1を見ると
https://codeberg.org/httpxyz/httpxyz/src/tag/0.31.1/httpxyz/_pytest_plugin.py#L7

import httpxyz

import httpxyzに秘密があります。
HTTPX Compatibility のドキュメントの冒頭より

>>> import sys
>>> import httpxyz
>>> sys.modules["httpxyz"]
<module 'httpxyz' from '/.../.venv/lib/python3.14/site-packages/httpxyz/__init__.py'>
>>> sys.modules["httpx"]
<module 'httpxyz' from '/.../.venv/lib/python3.14/site-packages/httpxyz/__init__.py'>
>>> sys.modules["httpx"] is sys.modules["httpxyz"]
True

importing httpxyz automatically registers it under the httpx name in sys.modules

sys.modulesとは
https://docs.python.org/ja/3/library/sys.html#sys.modules

This is a dictionary that maps module names to modules which have already been loaded.

import httpxyzhttpxyzだけでなくhttpxsys.modulesに登録され、import された扱いとなります。
その時、httpxモジュールの実態はhttpxyzです。置き換えられている!
https://codeberg.org/httpxyz/httpxyz/src/tag/0.31.1/httpxyz/__init__.py#L120-L121

if "httpx" not in _sys.modules:
    _sys.modules["httpx"] = _sys.modules[__name__]

import httpxしたときsys.modulesにあればそれを使う2ので、import httpxの裏では httpxyz が使われ続けます

終わりに

HTTPXYZ は pytest plugin も提供していました(pytest -p httpxyz)。
plugin の early load でimport httpxyzしてしまい、sys.modules["httpx"]も指定しています。
以降のimport httpxで import されるのは httpxyz モジュールとして実現していました

HTTPX に比べてパフォーマンス改善した HTTPXYZ を推していくにあたり、これは追い風です!

P.S. pytest-httpx も同様の対処がされている

HTTPX のモックライブラリには pytest-httpx もあります。
こちらについてもpytest -p httpxyzで HTTPXYZ と合わせて使えるようにしたそうです。
https://httpxyz.org/httpx-compatibility/#using-pytest-httpx-in-pytest


  1. 設定箇所 https://codeberg.org/httpxyz/httpxyz/src/tag/0.31.1/pyproject.toml#L66-L67
  2. インポートではモジュール名は sys.modules から探され、存在した場合は、対応する値がインポートされるべきモジュールであり、この処理は完了します。」ref: https://docs.python.org/ja/3/reference/import.html#the-module-cache