비동기 테스트
비동기 테스트 (Async Tests)
FastAPI 애플리케이션을 제공되는 TestClient로 테스트하는 방법은 이미 봤어요. 지금까지는 async 함수를 쓰지 않는 동기(synchronous) 테스트만 봤죠.
테스트에서 비동기 함수를 쓸 수 있으면 유용할 때가 있어요. 예를 들어 데이터베이스를 비동기로 조회할 때 말이에요. FastAPI 애플리케이션에 요청을 보내고, 그다음에 백엔드가 데이터베이스에 올바른 데이터를 성공적으로 썼는지 확인하고 싶다고 상상해 보세요. 비동기 데이터베이스 라이브러리를 쓰면서요.
이걸 어떻게 동작시키는지 살펴볼게요.
출처: 공식문서
pytest.mark.anyio
테스트에서 비동기 함수를 호출하려면, 테스트 함수 자체가 비동기여야 해요. AnyIO가 이걸 위한 깔끔한 플러그인을 제공해요. 이 플러그인으로 특정 테스트 함수들을 비동기로 호출하도록 지정할 수 있어요.
HTTPX
FastAPI 애플리케이션이 async def 대신 일반 def 함수를 사용하더라도, 이 애플리케이션은 밑에서는 여전히 async 애플리케이션이에요.
TestClient는 내부적으로 마법을 부려서, 표준 pytest를 쓰는 일반 def 테스트 함수 안에서 비동기 FastAPI 애플리케이션을 호출해 줘요. 하지만 그 마법은 비동기 함수 안에서 사용할 때는 더 이상 동작하지 않아요. 테스트를 비동기로 실행하면, 테스트 함수 안에서 더 이상 TestClient를 사용할 수 없는 거예요.
TestClient는 HTTPX를 기반으로 해요. 다행히 HTTPX를 직접 사용해서 API를 테스트할 수 있어요.
예시
간단한 예시로, Bigger Applications와 Testing에서 설명한 것과 비슷한 파일 구조를 생각해 볼게요.
.
├── 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.py 파일은 main.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"}
실행하기
평소처럼 테스트를 실행하면 돼요.
$ uv run 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"}
이건 우리가 TestClient로 요청할 때 쓰던 것과 같은 의미예요.
response = client.get('/')
팁
새
AsyncClient로 async/await를 쓰고 있다는 점에 주목하세요. 요청이 비동기예요.
경고
애플리케이션이 lifespan 이벤트에 의존한다면,
AsyncClient는 그 이벤트들을 트리거하지 않아요. 이벤트들이 확실히 실행되게 하려면 florimondmanca/asgi-lifespan의LifespanManager를 사용하세요.
다른 비동기 함수 호출하기
테스트 함수가 이제 비동기이므로, FastAPI 애플리케이션에 요청을 보내는 것 외에 다른 async 함수들도 테스트 안에서 호출하고(await) 할 수 있어요. 코드의 다른 곳에서 호출하는 것과 똑같이요.
팁
테스트에 비동기 함수 호출을 통합할 때
RuntimeError: Task attached to a different loop를 만나면(예: MongoDB의 MotorClient를 쓸 때), 이벤트 루프가 필요한 객체들은 비동기 함수 안에서만 생성해야 한다는 점을 기억하세요. 예를 들어@app.on_event("startup")콜백 안에서요.
더 알아보기 (Learn more)
이 문서는 FastAPI 공식 문서 - Async Tests를 한국어로 정리한 번역이에요. 원문에서 최신 내용과 더 다양한 예시를 확인하세요.