はじめに
新宿御苑.wasm、ありがとうございました!刺激的だった〜 nikkieです。
先日Groqへのリクエストを例に、HTTPクライアント HTTPXをRESPXでモックしてテストを書きました。
その中の宿題事項にアプローチします。
ヘッダやJSON形式のデータもモックに指定できるか
目次
- はじめに
- 目次
- 結論:宿題に取り組んだ後のテストコード
- 前回のテストコード
- 環境変数GROQ_API_KEYはmonkeypatch.setenv()
- RESPXできめ細かなモック
- mock()に代えてrespond()
- まとめ:リクエストのヘッダやデータもassertするテストコード
- 終わりに
結論:宿題に取り組んだ後のテストコード
メンテしているSpeechRecognitionより
https://github.com/Uberi/speech_recognition/blob/3.12.0/tests/recognizers/test_groq.py
@respx.mock(assert_all_called=True, assert_all_mocked=True) def test_transcribe_with_groq_whisper(respx_mock, monkeypatch): monkeypatch.setenv("GROQ_API_KEY", "gsk_grok_api_key") respx_mock.post( "https://api.groq.com/openai/v1/audio/transcriptions", headers__contains={"Authorization": "Bearer gsk_grok_api_key"}, data__contains={"model": "whisper-large-v3"}, ).mock( return_value=httpx.Response( 200, json={ "text": "Transcription by Groq Whisper", "x_groq": {"id": "req_unique_id"}, }, ) ) # 省略
前回のテストコード
transcribe.py
from groq import Groq def transcribe(filename: str) -> str: client = Groq() with open(filename, "rb") as f: transcription = client.audio.transcriptions.create( file=(filename, f.read()), model="whisper-large-v3-turbo", ) return transcription.text if __name__ == "__main__": print(transcribe("sample.wav"))
test_transcribe.py
動作環境
- Python 3.12.6
- groq 0.13.0
- httpx 0.27.2
- respx 0.21.1
- pytest 8.3.4
環境変数GROQ_API_KEY
はmonkeypatch.setenv()
groq.Groq()
は環境変数GROQ_API_KEY
が設定されていないとgroq.GroqError
を送出します。
>>> import groq >>> groq.Groq() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/.../.venv/lib/python3.12/site-packages/groq/_client.py", line 89, in __init__ raise GroqError( groq.GroqError: The api_key client option must be set either by passing api_key to the client or by setting the GROQ_API_KEY environment variable
pytestのフィクスチャのmonkeypatchを使って、テスト中にダミーの値を設定することにしました。
https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables
HTTPリクエストはRESPXがモックする(=テストでは通信しない)ので、ダミーの値で問題ありません
RESPXできめ細かなモック
RESPXのドキュメントを見ていたところ、リクエストのヘッダやボディに特定のデータが含まれている場合にモックできることが分かりました。
https://lundberg.github.io/respx/api/#patterns
モックする際に今回見る項目
- headers
- data
モックの仕方
こんなモックが可能ということです!
headers__eq=...
- ヘッダが一致する場合にモック
headers__contains=...
- ヘッダに含む場合にモック
headers=...
はこちらの意味とのこと
data__eq=...
- 送信するデータが一致する場合にモック
data=...
はこちらの意味とのこと
data__contains=...
- 送信するデータに含む場合にモック
@respx.mock(assert_all_called=True, assert_all_mocked=True) def test_transcribe(respx_mock, monkeypatch): monkeypatch.setenv("GROQ_API_KEY", "gsk_grok_api_key") respx_mock.post( "https://api.groq.com/openai/v1/audio/transcriptions", + headers__contains={"Authorization": "Bearer gsk_grok_api_key"}, + data__contains={"model": "whisper-large-v3-turbo"}, ).mock(
追加したのは2点
- Authorizationヘッダを含むときモック
- リクエストのデータが指定したモデルを含む時にモック
試行錯誤していたときはモックが当たらず、「groq.APIConnectionError: Connection error.」をよく見かけました(時間がかかるのでおそらく通信しようとしている)
mock()
に代えてrespond()
RESPXのドキュメントを見ていて知りました。
https://lundberg.github.io/respx/api/#respond
Shortcut for creating and mocking a HTTPX Response.
mock()
のreturn_value
としてhttpx.Response
をインスタンス化する代わりに
respx_mock.post( # 省略 ).respond( 200, json={ "text": "Your transcribed text appears here...", "x_groq": {"id": "req_unique_id"}, }, )
と書けます。
まとめ:リクエストのヘッダやデータもassertするテストコード
終わりに
RESPXについて知識を深めました。
headers
やdata
をcontains
やeq
で検証しつつモックできるheaders__contains
という規則
respond()
を使うと、httpx.Response
をインスタンス化するよりもスッキリ書ける
なお、残っている宿題は以下です
sample.wavを不要にできる?