Endpoint FastAPI yang berjalan di production tanpa test itu seperti jembatan yang belum diinspeksi. Semua terasa aman sampai satu perubahan kecil mengubah struktur response JSON, dan yang menemukannya duluan justru klienmu, bukan kamu. Kabar baiknya: menulis test untuk FastAPI tidak lebih sulit daripada menulis endpoint-nya. FastAPI menyediakan TestClient yang dibangun di atas httpx, dan pytest yang biasa kamu pakai untuk testing Python langsung bisa dipakai di sini.

Siapkan Proyek

Mulai dari folder kosong. Saya pakai venv bawaan Python, tapi kalau kamu sudah terbiasa dengan uv hasilnya sama saja. Yang penting tiga paket ini terpasang: fastapi, pytest, dan httpx (TestClient butuh httpx di balik layar).

bash
mkdir fastapi-testing && cd fastapi-testing
python -m venv .venv && source .venv/bin/activate
pip install fastapi pytest httpx

pytest --version

Aplikasi yang Akan Diuji

Biar punya sesuatu untuk diuji, bikin API toko sederhana dengan dua endpoint: GET untuk membaca item dan POST untuk menambah item. Kalau kamu belum pernah menyentuh FastAPI, tutorial FastAPI dari nol adalah titik awal yang lebih pas. Simpan file ini sebagai main.py.

python
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


def get_db():
    # Di dunia nyata ini jadi koneksi SQLAlchemy ke PostgreSQL
    db = {"items": {1: {"name": "Buku", "price": 50000.0}}}
    yield db


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/items/{item_id}")
def read_item(item_id: int, db=Depends(get_db)):
    item = db["items"].get(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"id": item_id, **item}


@app.post("/items")
def create_item(item: Item, db=Depends(get_db)):
    item_id = max(db["items"], default=0) + 1
    db["items"][item_id] = item.model_dump()
    return {"id": item_id, **item.model_dump()}

Test Pertama dengan TestClient

TestClient membungkus aplikasi FastAPI dan memungkinkan kamu memanggil endpoint lewat .get(), .post(), dan seterusnya, tanpa server yang benar-benar jalan. Responsenya mirip response httpx: ada .status_code dan .json(). Simpan sebagai test_main.py.

python
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def test_health():
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


def test_read_item_ditemukan():
    response = client.get("/items/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Buku"


def test_read_item_tidak_ada():
    response = client.get("/items/999")
    assert response.status_code == 404
    assert response.json()["detail"] == "Item not found"

Jalankan pytest. Tiga test hijau berarti kamu sudah punya jaring pengaman pertama — siapa pun yang mengubah main.py nanti akan langsung tahu kalau ada yang rusak.

bash
pytest -v

Fixture: Jangan Ulangi TestClient di Setiap File

Menulis client = TestClient(app) di setiap file lama-lama berulang. Pindahkan ke conftest.py supaya pytest otomatis menyediakannya sebagai fixture bernama client, lengkap dengan lifecycle yang benar lewat context manager.

python
import pytest
from fastapi.testclient import TestClient

from main import app


@pytest.fixture
def client():
    with TestClient(app) as c:
        yield c

Sekarang test cukup menerima client sebagai parameter. Fixture ini dipanggil ulang untuk tiap test, jadi tidak ada state yang bocor dari satu test ke test berikutnya.

Test POST dan Validasi Pydantic

POST lebih menarik karena melibatkan validasi Pydantic. Kirim JSON yang benar dan test jalur suksesnya; kirim data yang salah dan pastikan FastAPI menolak dengan status 422.

python
def test_create_item_sukses(client):
    response = client.post("/items", json={"name": "Pensil", "price": 2500})
    assert response.status_code == 200
    body = response.json()
    assert body["id"] == 2
    assert body["name"] == "Pensil"
    assert body["price"] == 2500.0


def test_create_item_harga_invalid(client):
    response = client.post("/items", json={"name": "Pensil", "price": "mahal"})
    assert response.status_code == 422

Lelah menjalankan seluruh test hanya untuk satu perubahan? pytest test_main.py::test_create_item_sukses -v menjalankan satu test saja. Tambahkan -x supaya pytest berhenti di kegagalan pertama.

Dependency Override: Putus Koneksi ke Database Asli

Lihat lagi get_db di main.py. Endpoint membaca data lewat dependency itu, dan di dunia nyata fungsinya terhubung ke database sungguhan. Test tidak boleh menyentuh database production, jadi kita ganti dependency-nya lewat app.dependency_overrides. Pastikan juga kamu membersihkannya setelah test selesai supaya tidak merembet ke test lain.

python
import pytest
from fastapi.testclient import TestClient

from main import app, get_db


@pytest.fixture
def client_db_kosong():
    def override_get_db():
        yield {"items": {}}

    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()


def test_create_item_ke_database_kosong(client_db_kosong):
    response = client_db_kosong.post(
        "/items", json={"name": "Pensil", "price": 2500}
    )
    assert response.status_code == 200
    assert response.json() == {"id": 1, "name": "Pensil", "price": 2500.0}

Test yang menghantam database production atau API eksternal itu lambat dan flaky, plus kamu bisa tidak sengaja menulis data palsu ke sistem yang dipakai orang banyak. Selalu override dependency yang menyentuh dunia luar.

Ukur Coverage dengan pytest-cov

Test tanpa laporan coverage seperti berkendara tanpa speedometer — kamu tidak tahu bagian mana yang belum terlindungi. Pasang pytest-cov dan lihat baris mana yang belum tersentuh test.

bash
pip install pytest-cov
pytest --cov=main --cov-report=term-missing

Dari sini, langkah logis berikutnya adalah mencolokkan test ke pipeline CI supaya setiap push otomatis diuji. Kalau kamu pakai GitHub, tutorial GitHub Actions ini menunjukkan cara menjalankan pytest di workflow YAML. Mulai dari endpoint yang paling kritis di aplikasimu sekarang.