Menguji validasi dan error

Input buruk, field yang hilang, tipe yang salah, dan seperti apa error yang baik.

Pelajaran sebelumnya mengirim data yang baik dan memeriksa bahwa data itu tersimpan. Pelajaran ini mengirim data yang buruk dan memeriksa bahwa data itu ditolak. Di sebagian besar proyek, di sinilah bug berada. Developer menulis jalur normal lebih dulu, mengujinya secara manual sambil membangun, lalu kehabisan waktu sebelum sempat memikirkan apa lagi yang bisa dikirim klien.

Tes validasi punya dua bagian. Server harus menolak request dengan status yang benar, dan tidak boleh menyimpan apa pun. Setelah itu, kualitas pesan error-nya penting, karena suatu saat developer lain akan membacanya sambil mencoba memahami apa yang ia lakukan salah.

Formulir dengan aturan

POST /api/validation/registermasuk untuk melihat adalah endpoint pendaftaran dengan aturan tertulis. Halaman referensinya mendaftar aturannya: username terdiri dari 3 sampai 20 huruf, angka, atau garis bawah, usia adalah bilangan bulat dari 18 sampai 120, negara adalah salah satu dari sembilan kode, dan seterusnya. Tidak ada yang disimpan, jadi Anda bisa mengirim body yang sama sesering yang Anda mau. Request yang valid:

POST/api/validation/register
{
  "username": "ava_t",
  "email": "ava@example.com",
  "password": "Str0ng!Pass",
  "confirmPassword": "Str0ng!Pass",
  "age": 29,
  "country": "us",
  "phone": "+15550101",
  "acceptTerms": true
}
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
Response
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "message": "User registered successfully",
  "data": {
    "id": 1001,
    "username": "ava_t",
    "email": "ava@example.com",
    "age": 29,
    "country": "US",
    "createdAt": "2026-09-30T18:32:12Z"
  }
}

Perhatikan bahwa "us" kembali sebagai "US". Server menormalkan nilainya alih-alih menolaknya. Itu tidak masalah, tetapi itu sebuah perilaku, dan tes sebaiknya menguncinya agar tidak ada yang mengubahnya tanpa sengaja.

Sekarang langgar hampir semua aturan sekaligus:

POST/api/validation/register
{
  "username": "ab",
  "email": "ava@example",
  "password": "password",
  "confirmPassword": "Password",
  "age": 17,
  "country": "XX",
  "acceptTerms": false
}
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
Response
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/validation-error",
  "title": "One or more fields failed validation.",
  "status": 422,
  "detail": "7 field(s) failed validation.",
  "instance": "/api/validation/register",
  "errors": {
    "username": ["Username must be 3-20 characters long."],
    "email": ["Email must be a valid email address."],
    "password": [
      "Password must contain an uppercase letter.",
      "Password must contain a digit.",
      "Password must contain a special character."
    ],
    "confirmPassword": ["Passwords do not match."],
    "age": ["Age must be between 18 and 120."],
    "country": ["Country must be one of: US, GB, IN, DE, FR, JP, CA, AU, BR."],
    "acceptTerms": ["You must accept the terms and conditions."]
  }
}
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Ini response error yang baik, dan layak dilihat mengapa, agar nanti Anda bisa mengenali yang buruk.

  • Ia melaporkan setiap masalah sekaligus. Klien yang memperbaiki formulir tidak perlu mengirimnya tujuh kali untuk menemukan tujuh kesalahan.
  • Setiap error dikunci dengan nama field yang dikirim klien, sehingga antarmuka bisa menampilkannya di samping input yang tepat.
  • Pesannya menyebut aturannya ("between 18 and 120") alih-alih bilang nilainya salah dan membiarkan klien menebak alasannya.
  • Ia mengikuti RFC 7807 (diperbarui sebagai RFC 9457), format standar untuk error HTTP dengan field type, title, status, detail, dan instance, serta media type application/problem+json. Anda tidak perlu memaksakan format ini, tetapi jika sebuah API mengaku memakainya, periksa bahwa field-field itu ada dan bahwa status di body sama dengan kode status sebenarnya.

Apa yang dikirim

Anda tidak akan mencoba segalanya di setiap field. Telusuri setiap field dan tanyakan mana dari ini yang mungkin berlaku:

Jenis inputContoh
HilangTinggalkan field-nya sama sekali. Kirim sebagai null. Kirim string kosong. Ketiganya bisa berperilaku berbeda.
Tipe salah"29" untuk angka, 29 untuk string, true untuk string, objek di tempat yang seharusnya daftar
BatasNilai minimum dan maksimum persis, satu di kedua sisinya, nol, angka negatif, desimal di tempat yang seharusnya bilangan bulat
PanjangKosong, satu karakter, panjang maksimum, satu lebih, dan sesuatu yang sangat panjang (10.000 karakter)
FormatEmail, tanggal, nomor telepon, URL, dan kode yang hampir benar
Spasi dan huruf besar-kecilSpasi di depan, judul yang hanya berisi spasi, Admin saat admin sudah dipakai
Field tak dikenalNama field yang salah eja, field tambahan, field yang seharusnya tidak bisa diatur klien
Body itu sendiriTanpa body, JSON rusak, array alih-alih objek, Content-Type yang salah

Batas pada endpoint pendaftaran

Aturan usianya 18 sampai 120, jadi nilai yang menarik adalah 17, 18, 120, dan 121. Di API ini, 18 dan 120 diterima, sementara 17 dan 121 mendapat "Age must be between 18 and 120." Nilai 18.5mendapat pesan lain, "Age must be an integer.", begitu juga string "29". Beberapa API diam-diam mengonversi "29" menjadi 29. Tak satu pun perilaku itu salah dengan sendirinya, tetapi klien akan bergantung pada yang mana pun yang dirilis.

Username terdiri dari 3 sampai 20 karakter: abc dan nama 20 huruf lolos, nama 21 huruf gagal. Username dengan spasi ditolak dengan pesan tentang karakter yang diizinkan. Nama admin yang sudah dipakai ditolak, dan Admin juga, yang memberi tahu Anda bahwa pemeriksaan keunikan mengabaikan huruf besar-kecil. Jika tidak, dua pengguna bisa berakhir dengan nama yang terlihat sama di hampir semua tempat.

Pemeriksaan email seperti biasa kacau. ava@example, ava@@example.com, dan ava.example.com ditolak, begitu juga " ava@example.com" dengan spasi di depan. ava+qa@example.com dan AVA@EXAMPLE.COM diterima, dan itu benar. a@b.c juga diterima. Validasi email yang sesungguhnya adalah perdebatan panjang yang tak dimenangkan siapa pun, jadi tesnya tidak terlalu soal aturan yang sempurna, melainkan soal kasus yang penting bagi produk Anda: alamat dengan tanda plus, huruf besar, dan apakah spasi di sekelilingnya dipangkas atau ditolak.

Kosong tidak sama dengan hilang

Kirim {} dan setiap field wajib dilaporkan sebagai "is required". Kirim field yang sama dengan nilai null dan Anda mendapat hasil yang sama. Itu konsisten, dan itu bagus. Di /api/Booksmasuk untuk melihat, judul berisi tiga spasi juga diperlakukan sebagai hilang. Banyak API hanya memeriksa bahwa sebuah string ada, sehingga judul berisi spasi, atau string kosong, lolos begitu saja dan kemudian muncul sebagai baris kosong di tabel seseorang.

Batas panjang sering tidak ada sama sekali. /api/Booksmasuk untuk melihat menerima judul 5.000 karakter dengan 201. Tidak ada batas yang didokumentasikan, jadi mungkin itu disengaja, tetapi itu jenis hal yang nanti tidak akan disetujui kolom database atau tata letak antarmuka. Tanyakan berapa seharusnya batasnya, dan uji satu karakter di kedua sisinya.

Field tak dikenal

Saat klien mengirim field yang tidak dikenal server, server bisa mengabaikan atau menolaknya. Di pelajaran CRUD, /api/Booksmasuk untuk melihat mengabaikan field year yang salah eja dan menyimpan tahun 0. /api/validation/strictmasuk untuk melihat justru menolak:

Terminal
curl -X POST https://api.testingapis.com/api/validation/strict \
  -H "Content-Type: application/json" \
  -d '{"name": "Widget", "email": "a@b.co", "quantity": 2, "discount": 50}'

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/unknown-fields",
  "title": "Request body contains unknown fields.",
  "status": 400,
  "detail": "Allowed fields: name, email, quantity (names are case-sensitive).",
  "instance": "/api/validation/strict",
  "unknownFields": ["discount"]
}
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Nama field di sini membedakan huruf besar-kecil, jadi Name alih-alih name juga merupakan field tak dikenal. Menolak lebih aman, karena salah ketik langsung terlihat. Mengabaikan lebih ramah saat API berkembang dan klien lama mengirim field yang sudah dihapus. Tugas Anda adalah mencari tahu pendekatan mana yang dipakai API dan apakah dipakai secara konsisten di semua endpoint.

Ada satu jenis field tak dikenal yang merupakan masalah keamanan, bukan soal gaya: field yang ada di data tetapi seharusnya tidak bisa diatur klien, seperti role, isAdmin, price pada pesanan, atau id. Jika server menyalin setiap field yang masuk ke data, pengguna bisa menaikkan hak aksesnya sendiri. Pelajaran keamanan menyebutnya mass assignment.

Body yang rusak

Berikutnya, uji request secara keseluruhan. Ini response sungguhan dari endpoint pendaftaran:

Response
# No body at all
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"":["A non-empty request body is required."]},"traceId":"00-fc01be6d..."}

# {"username":"ava_t",}  (trailing comma)
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"$":["The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 0 | BytePositionInLine: 20."]},"traceId":"00-e13ac011..."}

# [] instead of an object
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"status":400,"error":"Bad Request","message":"Request body must be a JSON object."}

# Valid JSON sent as Content-Type: text/plain
HTTP/1.1 415 Unsupported Media Type
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported Media Type","status":415,"traceId":"00-4cadc1e1..."}

Keempatnya ditangani, dan 415 untuk Content-Type yang salah tepat sekali. Tetapi lihat bentuknya. Tiga response berupa problem+json dengan peta errors. Kasus array mengembalikan format yang sama sekali lain, {status, error, message}, dengan content type JSON biasa. Klien yang mem-parse error dengan satu cara akan rusak pada cara lainnya. Ini terjadi saat sebagian error berasal dari framework dan sebagian lain dari kode aplikasi, dan ini salah satu temuan paling umum dalam pengujian API. Kumpulkan bentuk error yang Anda lihat di sebuah API dan jajarkan berdampingan.

Apa yang tidak boleh ada di error

Ini /api/Booksmasuk untuk melihat yang menerima string di tempat yang mengharapkan angka:

Terminal
curl -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{"title": "x", "author": "y", "pages": "many"}'

HTTP/1.1 400 Bad Request

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "item": ["The item field is required."],
    "$.pages": ["The JSON value could not be converted to System.Int32. Path: $.pages | LineNumber: 0 | BytePositionInLine: 40."]
  },
  "traceId": "00-d027cc55a2925746a1bf93cc96d2306d-02234fa55d940604-00"
}
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Statusnya benar. Isinya punya dua masalah. Pesannya menyebut System.Int32, yang memberi tahu orang luar bahwa server berjalan di .NET dan meninggalkan klien dengan nama tipe alih-alih "pages harus berupa bilangan bulat". Dan error tambahan "The item field is required." merujuk ke nama parameter internal, bukan sesuatu yang dikirim klien. Keduanya tidak berbahaya di sini, tetapi pada produk sungguhan Anda akan melaporkan keduanya.

Hal yang tidak boleh pernah muncul di error adalah stack trace, potongan SQL, path file, nama host internal, dan nilai rahasia. Sebaliknya, traceId berguna: ia memungkinkan tim dukungan menemukan log server yang cocok tanpa membocorkan apa pun.

Kode status yang mana

Tim sering berdebat soal 400 versus 422. Pembagian yang umum adalah 400 untuk body yang tidak bisa dibaca server (JSON rusak, tipe salah) dan 422 untuk body yang terbaca tetapi melanggar aturan (usia 17). /api/validation/registermasuk untuk melihat mengembalikan 400 hanya saat ia sama sekali tidak bisa mem-parse body, dan 422 untuk semua hal lain, termasuk string di tempat usia. /api/validation/annotationsmasuk untuk melihat memakai 400 untuk semuanya. Kedua pendekatan bisa diterima jika konsisten dan didokumentasikan, dan dua endpoint di API yang sama yang melakukannya secara berbeda layak dipertanyakan.

StatusArtinya untuk input buruk
400 Bad RequestRequest-nya rusak atau tidak valid. Klien harus mengubahnya.
415 Unsupported Media TypeContent-Type-nya bukan yang diterima endpoint.
422 Unprocessable ContentBody-nya bisa di-parse, tetapi nilainya melanggar aturan.
409 ConflictValid dengan sendirinya, tetapi bentrok dengan keadaan saat ini, seperti duplikat. (Endpoint pendaftaran memakai 422 untuk username yang sudah dipakai.)
500 Internal Server ErrorTidak pernah benar untuk input buruk. Server crash karena sesuatu yang seharusnya ditolaknya.

Baris terakhir adalah satu-satunya aturan tanpa pengecualian. Jika input apa pun yang Anda kirim menghasilkan 500, Anda telah menemukan bug, seaneh apa pun input-nya. Server seharusnya menolaknya, tetapi malah tumbang. Anda bisa melihat seperti apa 500 yang terbentuk dengan baik di /api/validation/problem/internalmasuk untuk melihat, dan contoh dokumen problem lainnya didaftar di sini:

GET/api/validation/problem
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Latihan

Uji POST /api/validation/annotationsmasuk untuk melihat. Contoh validnya ada di halaman referensi. Endpoint ini memakai validasi bawaan framework alih-alih aturan yang ditulis tangan.

  • Temukan batas untuk username, age, dan tags.
  • Coba tiga nomor telepon dan situs web yang tidak valid serta dua yang valid.
  • Kirim body yang tidak valid ke /annotations dan /register lalu bandingkan response error-nya. Status apa yang dipakai masing-masing? Bagaimana nama field di errors dieja dibandingkan nama field yang Anda kirim?
  • Kirim string di tempat age mengharapkan angka. Apakah pesannya membocorkan sesuatu?

Tuliskan perbedaan antara kedua endpoint seolah Anda melaporkannya kepada developer: apa yang Anda kirim, apa yang kembali, dan apa yang Anda harapkan.

Berlatih dan melangkah lebih jauh