Menguji aplikasi FastAPI dengan TestClient (httpx) dan pytest: fixture database terisolasi, dependency override untuk memalsukan database, monkeypatch untuk mengganti perilaku, dan pola test untuk CRUD lengkap.

Setelah di episode 9 kita membangun CRUD lengkap dengan SQLAlchemy, sekarang kita buktikan bahwa aplikasi itu benar-benar bekerja — dengan testing. Banyak developer melewati tahap ini demi kecepatan, padahal justru di sinilah perbedaan antara "jalan di laptop" dan "bisa dideploy dengan percaya diri".
Mengapa episode ini penting? Karena FastAPI memiliki salah satu keunggulan testing terbaik di dunia web Python: dependency override. Logika yang di episode 9 bergantung pada database bisa diganti total di dalam test tanpa mengubah satu baris kode aplikasi. Inilah arsitektur DI (episode 8) yang mulai terbayar.
pip install pytest httpxFastAPI 0.141 menyediakan TestClient yang dibangun di atas httpx — klien HTTP yang bisa menjalankan aplikasi FastAPI secara in-memory, tanpa server sungguhan.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_root() -> None:
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Halo Dunia FastAPI"}TestClient(app) membungkus aplikasi dan mengirim request langsung lewat ASGI — response-nya objek httpx dengan .status_code, .json(), dan .headers. Uji berjalan tanpa membuka port, jadi cepat dan aman dijalankan di CI.
Tip
TestClient juga mengeksekusi seluruh pipeline — dependency, middleware, dan validasi. Jadi test GET / di atas sudah memverifikasi hot path FastAPI, bukan hanya fungsi handler.
Test tidak boleh mengotori database sungguhan. Pola klasik: pakai SQLite in-memory yang dibuat ulang per test:
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from fastapi.testclient import TestClient
@pytest.fixture()
def db_session():
engine = create_engine(
"sqlite://", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
TestingSession = sessionmaker(bind=engine)
session = TestingSession()
try:
yield session
finally:
session.close()
Base.metadata.drop_all(engine)
@pytest.fixture()
def client(db_session):
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()Baris paling penting: app.dependency_overrides[get_db] = override_get_db. Ini memberitahu FastAPI: untuk fungsi dependency get_db, pakai versi ini — sesi test — bukan yang asli. Semua handler di episode 9 yang memakai Depends(get_db) otomatis terhubung ke database test.
Important
dependency_overrides adalah peta dari fungsi dependency asli ke penggantinya, dan harus dibersihkan setelah test (app.dependency_overrides.clear()) agar tidak bocor ke test berikutnya. Inilah mengapa override di-fixture, bukan di dalam fungsi test.
def test_create_item(client) -> None:
response = client.post(
"/items/",
json={"name": "Keyboard", "price": 500000},
)
assert response.status_code == 201
body = response.json()
assert body["name"] == "Keyboard"
assert body["id"] == 1
def test_read_missing_item_returns_404(client) -> None:
response = client.get("/items/999")
assert response.status_code == 404
def test_create_item_with_bad_price(client) -> None:
response = client.post(
"/items/",
json={"name": "Keyboard", "price": -5},
)
assert response.status_code == 422Tiga test ini mencakup tiga jalur berbeda: sukses (201), tidak ketemu (404), dan validasi gagal (422). Perhatikan bahwa kita tidak menebak status code — FastAPI dan kode kita sendiri yang menentukan, dan test mengunci perilaku itu agar tidak berubah diam-diam.
Terkadang perlu mengganti fungsi tertentu — misalnya memalsukan waktu atau klien eksternal — tanpa menyentuh database. monkeypatch pytest adalah alatnya:
import time
def _palsu_waktu():
return 1_700_000_000
def test_timestamp(monkeypatch, client) -> None:
monkeypatch.setattr(time, "time", _palsu_waktu)
response = client.get("/items/")
assert response.status_code == 200monkeypatch.setattr(time, "time", _palsu_waktu) menimpa time.time selama test berjalan, lalu memulihkannya otomatis setelah selesai — tanpa efek samping ke test lain. Ini pola utama untuk menguji kode yang bergantung pada jam, random, atau API eksternal.
fastapi-lab/
app/
main.py
database.py
models.py
schemas.py
deps.py
tests/
conftest.py
test_main.py
test_items.pyKonvensi pytest: file conftest.py berisi fixture bersama, file test_*.py berisi test. Jalankan seluruh suite:
pytest -vOutput harus menunjukkan semua test hijau. Di CI (episode 24), perintah ini menjadi gerbang sebelum deployment.
Warning
Jangan menulis test yang membaca database sungguhan. Test harus deterministik — hasilnya sama di laptop, CI, dan produksi. SQLite in-memory per-fixture di atas menjamin itu. Test yang bergantung environment hanya akan membuang waktu kalian saat berantakan.
| Praktik | Alasan |
|---|---|
| Test per status code | Mengunci kontrak API |
Fixture client dan db_session | Isolasi dan kecepatan |
dependency_overrides | Memalsukan resource tanpa refactor |
| Jalankan di CI | Mencegah regresi di setiap commit |
Inti yang harus dibawa pulang:
TestClient = httpx in-memory; cepat, tanpa port, mengeksekusi pipeline penuh.app.dependency_overrides[get_db] menggantikan sesi asli dengan sesi test — kekuatan DI.monkeypatch untuk mengganti fungsi runtime.Di episode 11 selanjutnya kita akan membahas middleware & CORS — menyisipkan logika di level HTTP sebelum dan sesudah routing, mengkonfigurasi CORS agar API bisa dipakai browser, serta membatasi host yang sah dengan TrustedHost. Setelah ini, API kalian siap "bergaul" dengan frontend!