コンテンツにスキップ

非同期テスト

🌐 AI と人間による翻訳

この翻訳は、人間のガイドに基づいて AI によって作成されました。🤝

原文の意図を取り違えていたり、不自然な表現になっている可能性があります。🤖

AI LLM をより適切に誘導するのを手伝う ことで、この翻訳を改善できます。

英語版

これまでに、提供されている TestClient を使って FastAPI アプリケーションをテストする方法を見てきました。ここまでは、async 関数を使わない同期テストのみでした。

テストで非同期関数を使えると、たとえばデータベースへ非同期にクエリする場合などに便利です。非同期データベースライブラリを使いながら、FastAPI アプリにリクエストを送り、その後バックエンドが正しいデータをデータベースに書き込めたかを検証したい、といったケースを想像してください。

その方法を見ていきます。

pytest.mark.anyio

テスト内で非同期関数を呼び出したい場合、テスト関数自体も非同期である必要があります。AnyIO はこれを実現するための便利なプラグインを提供しており、特定のテスト関数を非同期で呼び出すことを指定できます。

HTTPX

FastAPI アプリケーションが通常の def 関数を使っていても、その内側は依然として async アプリケーションです。

TestClient は、標準の pytest を使って通常の def のテスト関数から非同期の FastAPI アプリを呼び出すための「おまじない」を内部で行います。しかし、その「おまじない」はテスト関数自体が非同期の場合には機能しません。テストを非同期で実行すると、テスト関数内で TestClient は使えなくなります。

TestClientHTTPX を基に作られており、幸いなことに API のテストには HTTPX を直接利用できます。

簡単な例として、大きなアプリケーションテスト で説明したものに似たファイル構成を考えます:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py

main.py は次のようになります:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Tomato"}

test_main.pymain.py のテストを持ち、次のようになります:

import pytest
from httpx import ASGITransport, AsyncClient

from .main import app


@pytest.mark.anyio
async def test_root():
    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as ac:
        response = await ac.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Tomato"}

実行

テストはいつも通り次で実行できます:

$ pytest

---> 100%

詳細

マーカー @pytest.mark.anyio は、このテスト関数を非同期で呼び出すべきであることを pytest に伝えます:

import pytest
from httpx import ASGITransport, AsyncClient

from .main import app


@pytest.mark.anyio
async def test_root():
    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as ac:
        response = await ac.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Tomato"}

豆知識

TestClient を使っていたときと異なり、テスト関数は def ではなく async def になっている点に注意してください。

次に、アプリを渡して AsyncClient を作成し、await を使って非同期リクエストを送信できます。

import pytest
from httpx import ASGITransport, AsyncClient

from .main import app


@pytest.mark.anyio
async def test_root():
    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as ac:
        response = await ac.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Tomato"}

これは次と同等です:

response = client.get('/')

...これまでは TestClient でリクエストを送っていました。

豆知識

新しい AsyncClient では async/await を使っている点に注意してください。リクエストは非同期です。

注意

アプリケーションが lifespan イベントに依存している場合、AsyncClient はそれらのイベントをトリガーしません。確実にトリガーするには、florimondmanca/asgi-lifespanLifespanManager を使用してください。

その他の非同期関数呼び出し

テスト関数が非同期になったので、FastAPI アプリへのリクエスト送信以外の async 関数も、コードの他の場所と同様に呼び出して(await して)使えます。

豆知識

テストに非同期関数呼び出しを統合した際に(例: MongoDB の MotorClient 使用時)、RuntimeError: Task attached to a different loop に遭遇した場合は、イベントループを必要とするオブジェクトは非同期関数内でのみインスタンス化するようにしてください。例えば @app.on_event("startup") コールバック内で行います。