API yang belum teruji adalah API yang belum selesai. Skrip src/klien.py menguji layanan dari sisi pemanggil: tiga permintaan prediksi valid, satu permintaan sengaja tidak sah, dan satu health check. Pengujian memakai TestClient dari Starlette — seluruh tumpukan ASGI (routing, validasi Pydantic, handler) dijalankan di dalam proses yang sama tanpa membuka port, sehingga uji berjalan cepat dan bisa masuk rangkaian otomatis. Pendekatan ini adalah padanan lokal dari strategi uji layanan pada chapter REST API untuk Model
.
Klien Uji dalam Satu Berkas #
Inti klien mengirim data validasi sebagai permintaan nyata:
client = TestClient(app)
def main() -> None:
(_, _), (X_val, y_val) = siapkan_data()
indeks_uji = [1, 4, 7] # label 0, 5, 9; sampel 7 berprobabilitas rendah
print("== klien uji: request nyata ke /predict ==")
for i in indeks_uji:
# X_val ternormalisasi 0..1; API menerima nilai piksel asli 0..16
fitur = [int(round(v * 16.0)) for v in X_val[i]]
respons = client.post("/predict", json={"fitur": fitur})
hasil = respons.json()
label_asli = int(y_val[i])
cocok = "benar" if hasil["kelas"] == label_asli else "salah"
print(f"sampel {i:3d}: label asli {label_asli} -> "
f"{hasil['kelas']} ({hasil['label']}) [{cocok}], "
f"p={hasil['probabilitas']:.4f}, "
f"keyakinan_cukup={hasil['keyakinan_cukup']}, "
f"versi={hasil['versi']}")Satu baris pada klien ini menyimpan pelajaran preprocessing penting: X_val dari siapkan_data() sudah ternormalisasi 0..1, sedangkan kontrak API menerima piksel asli 0..16 — klien karenanya mengalikan kembali dengan 16 sebelum mengirim. Tanpa konversi ini seluruh prediksi akan tampak benar secara protokol namun salah secara angka, karena model menerima masukan yang jauh dari sebaran pelatihannya.
Jalankan dari folder proyek:
python3 src/klien.pyOutput yang Dihasilkan:
== klien uji: request nyata ke /predict ==
sampel 1: label asli 0 -> 0 (digit_0) [benar], p=0.9985, keyakinan_cukup=True, versi=1.0.0
sampel 4: label asli 5 -> 5 (digit_5) [benar], p=0.9977, keyakinan_cukup=True, versi=1.0.0
sampel 7: label asli 9 -> 9 (digit_9) [benar], p=0.8458, keyakinan_cukup=False, versi=1.0.0
== klien uji: request tidak valid ==
63 nilai -> 422 Value error, fitur harus berisi tepat 64 angka
== klien uji: health check ==
/health -> 200 {'status': 'ok', 'nama_model': 'mlp-digit-mini', 'versi': '1.0.0', 'akurasi_validasi': 0.9511}Membaca Ketiga Skenario #
Ketiga blok keluaran menguji tiga sifat layanan yang berbeda. Prediksi pertama dan kedua benar dengan probabilitas nyaris penuh (0.9985 dan 0.9977). Prediksi ketiga tetap benar secara kelas, tetapi probabilitasnya 0.8458 — di bawah ambang 0.9 — sehingga keyakinan_cukup bernilai False: model tetap menjawab, dan pemanggil diberi tanda untuk tidak mempercayai jawaban itu begitu saja. Perilaku bertingkat semacam ini lebih aman daripada menolak permintaan, karena keputusan akhir tetap berada di tangan pemanggil.
Permintaan 63 nilai ditolak dengan status 422 dan pesan validator Pydantic — bukan galat 500 dari dalam model. Pemisahan ini penting: kesalahan pemanggil tidak boleh menjatuhkan layanan, dan log server tetap bersih dari pengecualian yang sebenarnya bisa dicegah. Terakhir, /health menjawab 200 dengan metadata lengkap: status, nama model, versi, dan akurasi validasi yang dibaca dari metrik.json — informasi yang dipakai load balancer dan sistem pemantau sebagaimana dibahas pada chapter Monitoring Model Produksi
.
Layanan kini teruji dari sisi pemanggil. Perlu dicatat batas pendekatan TestClient: ia menguji seluruh tumpukan aplikasi, tetapi tanpa jaringan sungguhan — latensi HTTP, konkurensi, dan perilaku server uvicorn di bawah beban tidak ikut teruji. Untuk kebutuhan itu kita perlu menjalankan server nyata lalu mengukurnya dengan protokol yang dibahas pada chapter Optimasi Latensi . Satu persoalan lain juga masih terbuka: seberapa ringan dan cepat model ini bisa disajikan tanpa PyTorch — Bagian penutup men-export ONNX dan mengukur dampaknya.