API adalah kontrak: begitu klien bergantung, setiap perubahan pada endpoint adalah potensi kehancuran di sisi orang lain. Test otomatis menjaga kontrak itu tetap terpenuhi — setiap kali kode berubah, seluruh perilaku API diperiksa ulang dalam hitungan detik. Kabar baiknya, semua bahannya sudah kita miliki: TestClient dari chapter Membuat API dengan FastAPI memanggil aplikasi tanpa server, dan pytest adalah tool yang sama dengan Section Testing dan Code Quality (chapter Menulis Test dengan pytest dan Fixtures). Chapter ini menyusunnya menjadi berkas test utuh untuk API manajemen tugas, menjalankannya sungguhan, lalu menutup dengan daftar kasus tepi yang wajib diuji.

Fixture untuk TestClient #

Test API butuh dua prasyarat: aplikasi dalam keadaan bersih yang sama di setiap test, dan TestClient siap pakai. Keduanya digabung dalam satu fixture pytest. Bagian pertama berkas test berisi aplikasi yang diuji, termasuk autentikasi dari chapter Authentication dan Authorization:

python
import pytest
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel


class TugasBaru(BaseModel):
    judul: str


def api_key_valid(x_api_key: str = Header()) -> str:
    if x_api_key != "YOUR_API_TOKEN":
        raise HTTPException(status_code=401, detail="API key tidak valid")
    return x_api_key


app = FastAPI()
tugas_db: dict[int, dict] = {}


def reset_db():
    tugas_db.clear()
    tugas_db[1] = {"id": 1, "judul": "Belajar Python", "selesai": False}


@app.get("/tugas", dependencies=[Depends(api_key_valid)])
def daftar_tugas():
    return list(tugas_db.values())


@app.post("/tugas", status_code=201, dependencies=[Depends(api_key_valid)])
def buat_tugas(tugas: TugasBaru):
    id_baru = max(tugas_db, default=0) + 1
    tugas_db[id_baru] = {"id": id_baru, "judul": tugas.judul, "selesai": False}
    return tugas_db[id_baru]

File Test Utuh dan Hasil Eksekusinya #

Bagian kedua berisi fixture dan fungsi test. Setiap test mengikuti pola Arrange-Act-Assert: siapkan kondisi awal, lakukan satu aksi, lalu periksa hasilnya — komentarnya sengaja dituliskan agar strukturnya terlihat:

python
@pytest.fixture()
def client():
    reset_db()
    return TestClient(app)


def test_daftar_tugas_menampilkan_data_awal(client):
    # Arrange: database sudah berisi satu tugas lewat fixture
    # Act
    r = client.get("/tugas", headers={"X-API-Key": "YOUR_API_TOKEN"})
    # Assert
    assert r.status_code == 200
    assert r.json() == [{"id": 1, "judul": "Belajar Python", "selesai": False}]


def test_buat_tugas_mengembalikan_201(client):
    # Arrange
    body = {"judul": "Belajar REST"}
    # Act
    r = client.post("/tugas", json=body, headers={"X-API-Key": "YOUR_API_TOKEN"})
    # Assert
    assert r.status_code == 201
    assert r.json() == {"id": 2, "judul": "Belajar REST", "selesai": False}


def test_buat_tugas_tanpa_judul_ditolak(client):
    # Arrange: body tanpa field wajib
    body = {"selesai": True}
    # Act
    r = client.post("/tugas", json=body, headers={"X-API-Key": "YOUR_API_TOKEN"})
    # Assert
    assert r.status_code == 422


def test_api_key_hilang_ditolak(client):
    # Act: header X-API-Key tidak dikirim sama sekali
    r = client.get("/tugas")
    # Assert: FastAPI menolak header wajib yang hilang dengan 422, bukan 401
    assert r.status_code == 422


def test_api_key_palsu_ditolak(client):
    # Act
    r = client.get("/tugas", headers={"X-API-Key": "salah"})
    # Assert
    assert r.status_code == 401


def test_dependency_override_melewati_autentikasi(client):
    # Arrange: ganti dependensi API key dengan versi testing
    app.dependency_overrides[api_key_valid] = lambda: "kunci-testing"
    try:
        # Act: panggilan tanpa header X-API-Key
        r = client.get("/tugas")
        # Assert
        assert r.status_code == 200
    finally:
        app.dependency_overrides.clear()

Simpan seluruh isi di atas sebagai test_tasks.py, lalu jalankan:

bash
python3 -m pytest test_tasks.py -v

Output yang Dihasilkan:

text
============================= test session starts ==============================
platform linux -- Python 3.12.15, pytest-9.0.2, pluggy-1.6.0 -- /home/z/.venv/bin/python3
cachedir: .pytest_cache
...

collecting ... collected 6 items

test_tasks.py::test_daftar_tugas_menampilkan_data_awal PASSED            [ 16%]
test_tasks.py::test_buat_tugas_mengembalikan_201 PASSED                  [ 33%]
test_tasks.py::test_buat_tugas_tanpa_judul_ditolak PASSED                [ 50%]
test_tasks.py::test_api_key_hilang_ditolak PASSED                        [ 66%]
test_tasks.py::test_api_key_palsu_ditolak PASSED                         [ 83%]
test_tasks.py::test_dependency_override_melewati_autentikasi PASSED      [100%]

============================== 6 passed in 1.20s ===============================

Perhatikan fixture pada baris pertama blok test: setiap test menerima client yang segar — reset_db() dipanggil lebih dulu sehingga test tidak saling menular (kondisi awal selalu satu tugas ber-id 1), sesuai perilaku fixture function-scope pada chapter Fixtures.

Menguji Autentikasi dengan dependency_overrides #

Test terakhir layak dibedah terpisah. app.dependency_overrides menggantikan fungsi api_key_valid dengan stub yang selalu berhasil — teknik dari chapter Dependency Injection dan pola mock dari chapter Mocking dan Patching. Panggilan tanpa header X-API-Key pun lolos dengan 200, sehingga test lain boleh fokus pada logika bisnis tanpa repot kredensial. Blok finally wajib: tanpa app.dependency_overrides.clear(), override bocor ke test berikutnya dan membuatnya lulus karena alasan yang salah.

Kasus Tepi dan Cakupan Pengujian #

Daftar berikut adalah kasus tepi yang wajib diuji setiap endpoint API — masing-masing pernah muncul nyata di sepanjang Section ini:

  • Body tidak valid atau field wajib hilang → 422.
  • Header autentikasi tidak dikirim → 422 (perilaku asli FastAPI, seperti yang kita lihat) dan nilai salah → 401.
  • Peran tidak mencukupi → 403; resource tidak ada → 404.
  • Koleksi kosong → 200 dengan list kosong, bukan error.
  • Batas pagination: halaman terakhir, limit besar, page melebihi jumlah data.

Terakhir, sadari cakupan berkas ini: test lewat TestClient menembus routing, validasi Pydantic, dependency, hingga fungsi rute — level integration. Logika murni seperti perhitungan has_next pada chapter Pagination, Filtering, dan Sorting sebaiknya juga punya unit test terpisah tanpa HTTP sama sekali. Kombinasi keduanya menjaga kontrak dari luar dan logika dari dalam — dan kontrak sendiri akan terus berubah seiring usia API, uraian selanjutnya di chapter Versioning dan Backward Compatibility.