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:
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:
@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:
python3 -m pytest test_tasks.py -vOutput yang Dihasilkan:
============================= 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 →
200dengan list kosong, bukan error. - Batas pagination: halaman terakhir,
limitbesar,pagemelebihi 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.