ข้ามไปยังเนื้อหา

Testing ด้วย TestClient และ pytest

ใน Node คุณใช้ supertest ทำ HTTP requests กับ Express หรือ NestJS app ใน process และ Jest เป็น test runner ใน FastAPI คุณใช้ TestClient (จาก Starlette, bundled กับ FastAPI) สำหรับ in-process HTTP requests และ pytest เป็น test runner

TypeScript
// Jest + supertest — app.test.ts
import request from 'supertest';
import { app } from '../src/app';
describe('GET /items/:id', () => {
it('returns an item', async () => {
const res = await request(app).get('/items/1');
expect(res.status).toBe(200);
expect(res.body.id).toBe(1);
});
it('returns 404 for unknown id', async () => {
const res = await request(app).get('/items/9999');
expect(res.status).toBe(404);
});
});
Python
# pytest + TestClient — test_items.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_get_item():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json()["id"] == 1
def test_get_item_not_found():
response = client.get("/items/9999")
assert response.status_code == 404
TypeScript
// supertest POST
it('creates an item', async () => {
const res = await request(app)
.post('/items')
.send({ name: 'Widget', price: 9.99 })
.set('Content-Type', 'application/json');
expect(res.status).toBe(201);
expect(res.body.name).toBe('Widget');
});
it('rejects invalid input', async () => {
const res = await request(app)
.post('/items')
.send({ name: '', price: -1 });
expect(res.status).toBe(422);
});
Python
# TestClient POST
def test_create_item():
response = client.post(
"/items",
json={"name": "Widget", "price": 9.99},
)
assert response.status_code == 201
assert response.json()["name"] == "Widget"
def test_create_item_invalid():
response = client.post(
"/items",
json={"name": "", "price": -1},
)
assert response.status_code == 422

pattern การ testing ที่สำคัญใน FastAPI คือ app.dependency_overrides ซึ่งแทนที่ dependencies จริง (เช่น database session) ด้วย test doubles เทียบเท่ากับ jest.mock() หรือ overrideProvider() ของ NestJS

TypeScript
// NestJS — override provider ใน tests
const moduleRef = await Test.createTestingModule({
providers: [ItemsService],
})
.overrideProvider(DatabaseService)
.useValue({ findById: jest.fn().mockResolvedValue({ id: 1 }) })
.compile();
Python
# FastAPI — dependency_overrides
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db
# Fake DB session สำหรับ tests
def override_get_db():
yield FakeDBSession()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_get_item_with_fake_db():
response = client.get("/items/1")
assert response.status_code == 200
# Restore หลัง tests (หรือใช้ pytest fixture)
app.dependency_overrides.clear()

@pytest.fixture ของ pytest เทียบเท่ากับ beforeEach / afterEach ของ Jest ไฟล์ conftest.py แชร์ fixtures ข้าม test files เหมือน Jest global setup file

TypeScript
// Jest beforeEach / afterEach
let client: TestClient;
beforeAll(async () => {
const app = await NestFactory.create(AppModule);
await app.init();
client = app.getHttpServer();
});
afterAll(async () => { await app.close(); });
Python
# pytest — conftest.py fixtures
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine, get_db
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
TEST_DB_URL = "sqlite:///./test.db"
test_engine = create_engine(TEST_DB_URL, connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=test_engine)
@pytest.fixture(scope="module")
def client():
Base.metadata.create_all(bind=test_engine)
def override_db():
db = TestSession()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_db
with TestClient(app) as c:
yield c
Base.metadata.drop_all(bind=test_engine)

TestClient รัน async handler บน event loop ของตัวเอง ซึ่งเพียงพอสำหรับ test ส่วนใหญ่ แต่เมื่อตัว test เองต้องเป็น async เพราะต้อง await DB fixture หรือสั่งงาน background task ให้ขับ app ด้วย AsyncClient ของ httpx ผ่าน ASGITransport นี่คือวิธีมาตรฐานปัจจุบันสำหรับ async test ของ FastAPI และไม่ต้องรัน server จริง

TypeScript
// Node — supertest ค่อนข้าง sync; ถ้า setup เป็น async ก็ await เอง
import request from 'supertest';
test('GET /items', async () => {
await seedDatabase();
const res = await request(app).get('/items');
expect(res.status).toBe(200);
});
Python
# pytest + httpx AsyncClient — test_items_async.py
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.anyio
async def test_list_items(seeded_db):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/items")
assert response.status_code == 200
assert response.json() == []
# anyio ต้องระบุ backend; กำหนดครั้งเดียวใน conftest.py:
@pytest.fixture
def anyio_backend():
return "asyncio"

pytest.mark.anyio มากับ plugin anyio ที่ติดมากับ test dependencies ของ Starlette ส่วนอีกทางที่นิยมคือ plugin pytest-asyncio ที่ใช้ @pytest.mark.asyncio ไม่ว่าทางไหนคุณต้องบอก pytest ว่า test function เป็น coroutine ต่างจาก Jest ตรงที่ pytest เปล่า ๆ จะเก็บ coroutine ไว้เฉย ๆ โดยไม่ await

รันที่เครื่องตัวเอง — ต้องการ FastAPI + httpx + pytest ติดตั้ง: uv add --dev pytest pytest-cov httpx anyio (หรือ pip install ชุดเดียวกัน) Playground ข้ามในบทเรียนนี้: TestClient ต้องการ FastAPI app จริงและ HTTP lifecycle

FastAPI equivalent ของ supertest npm package สำหรับ in-process HTTP testing คืออะไร?
จะแทนที่ real database session ด้วย fake ใน FastAPI tests ได้อย่างไร?
ไฟล์ใดที่แชร์ pytest fixtures ข้าม test files ทั้งหมดในไดเรกทอรี?