API tidak punya halaman untuk dilihat manusia — kontraknya adalah status code, struktur JSON, dan header. Maka test API adalah kontrak yang dieksekusi: setiap endpoint kita kunci dengan assertion status, bentuk data-links-meta, pesan validasi 422, sampai envelope error kustom. Chapter HTTP Tests telah memperkenalkan assertJson; sekarang kita susun suite API lengkap dengan autentikasi token Sanctum, pagination, dan disiplin struktur. Semua contoh memakai resource Post yang sama dengan suite sebelumnya, dengan route routes/api.php berprefiks /api dan middleware auth:sanctum.
getJson dan Header Accept Otomatis #
Pasangan getJson, postJson, patchJson, dan deleteJson otomatis menyertakan header Accept: application/json — dan header inilah yang membuat Laravel merespons dalam JSON alih-alih mengalihkan seperti halaman web. Suite kita membuktikan bedanya lewat tamu yang mengetuk endpoint yang sama:
public function test_header_accept_mengubah_bentuk_penolakan_tamu(): void
{
$this->post('/api/posts', ['title' => 'Tanpa Accept', 'body' => 'Isi.'])
->assertRedirect('/login');
$this->postJson('/api/posts', ['title' => 'Dengan Accept', 'body' => 'Isi.'])
->assertUnauthorized();
}Request pertama tanpa Accept diperlakukan seperti pengunjung web: middleware auth mengalihkan ke /login (302). Request kedua dengan Accept JSON menerima badan 401 — dan assertUnauthorized() adalah pintasan untuk assertStatus(401).
Token Sanctum: actingAs vs Token Asli #
Untuk klien API, ada dua cara mengautentikasi test. Yang tercepat adalah Sanctum::actingAs() dengan daftar abilities, cocok menguji logika route:
public function test_abilities_membedakan_token_baca_dan_tulis(): void
{
$budi = $this->userBudi();
Sanctum::actingAs($budi, ['posts:read']);
$this->postJson('/api/posts', [
'title' => 'Ditolak Token Baca',
'body' => 'Token ini hanya boleh membaca.',
])->assertForbidden();
Sanctum::actingAs($budi, ['posts:create']);
$this->postJson('/api/posts', [
'title' => 'Belajar API Test',
'body' => 'Dibuat lewat postJson di dalam test.',
])->assertCreated()
->assertJsonPath('data.title', 'Belajar API Test');
}Route store kita dilindungi middleware abilities:posts:create, jadi token baca ditolak 403 dan token tulis lolos sampai 201 (assertCreated). Ketika yang diuji justru mekanisme token itu sendiri — bearer header, pencatatan di database — kita pakai token asli dari createToken() dengan helper withToken():
$token = $budi->createToken('ponsel-budi')->plainTextToken;
$this->getJson('/api/posts')->assertUnauthorized();
$this->withToken($token)->getJson('/api/posts')->assertOk();
$this->assertDatabaseHas('personal_access_tokens', [
'name' => 'ponsel-budi',
]);Request tanpa token ditolak 401, request dengan header Authorization: Bearer diterima, dan baris token tercatat di database — tiga bukti yang tidak bisa diberikan actingAs. Detail lengkap pola ini ada di chapter Pengujian Sanctum
.
Disiplin Struktur JSON: Pagination #
Index endpoint kita memakai PostResource::collection(Post::latest('id')->paginate(3)), sehingga response membentuk tiga key utama. Inilah bentuk nyatanya untuk lima post:
{
"data": [
{
"id": 5,
"title": "Post Pertama",
"published_at": "2026-01-10T08:00:00+00:00"
}
],
"links": {
"first": "http://localhost:8000/api/posts?page=1",
"last": "http://localhost:8000/api/posts?page=2",
"prev": null,
"next": "http://localhost:8000/api/posts?page=2"
},
"meta": {
"current_page": 1,
"last_page": 2,
"per_page": 3,
"total": 5
}
}Test-nya membagi verifikasi menjadi tiga lapis — jumlah, nilai, dan bentuk:
$this->getJson('/api/posts')
->assertOk()
->assertJsonCount(3, 'data')
->assertJsonPath('data.0.title', 'Post Pertama')
->assertJsonPath('meta.total', 5)
->assertJsonPath('meta.last_page', 2)
->assertJsonStructure([
'data' => [['id', 'title', 'published_at']],
'links' => ['first', 'last', 'prev', 'next'],
'meta' => ['current_page', 'from', 'last_page', 'path', 'per_page', 'to', 'total'],
]);assertJsonCount(3, 'data') membuktikan per_page dihormati; assertJsonPath mengunci nilai pada titik tertentu; assertJsonStructure memverifikasi kontrak bentuk tanpa peduli nilai — kombinasi ini mendeteksi kolom yang hilang sekaligus pengurutan yang salah.
Validasi 422 dan Envelope Error #
Validasi gagal pada request JSON membalas 422 dengan pesan per field. Bentuk nyatanya dari suite kita:
{
"message": "The title field is required. (and 1 more error)",
"errors": {
"title": ["The title field is required."],
"body": ["The body field is required."]
}
}Assertion-nya menyasar status, field yang salah, dan bentuk envelope sekaligus:
$this->postJson('/api/posts', ['title' => '', 'body' => ''])
->assertUnprocessable()
->assertJsonValidationErrors(['title', 'body'])
->assertJsonStructure(['message', 'errors' => ['title', 'body']]);
$this->assertDatabaseCount('posts', 0);Untuk error yang bukan validasi, aplikasi kita memakai envelope kustom di controller — show mencari post manual dan membalas 404 dengan kode error yang kita definisikan sendiri:
$this->getJson('/api/posts/999')
->assertNotFound()
->assertJson([
'error' => [
'code' => 'post_tidak_ditemukan',
'message' => 'Post tidak ditemukan.',
],
]);Pola assertJson di atas membandingkan subset — cukup untuk mengunci field penting tanpa menyalin seluruh response. Operasi patchJson dan deleteJson menutup suite: patch memperbarui judul dan meng-assert data.title baru, delete membalas {\"data\":{\"terhapus\":true}} lalu baris dipastikan hilang lewat assertDatabaseMissing.
Seluruh Suite Berjalan #
php artisan test tests/Feature/ApiPostTest.phpOutput yang Dihasilkan:
PASS Tests\Feature\ApiPostTest
✓ tamu ditolak 401 0.15s
✓ ponsel tanpa token ditolak dengan token diterima 0.03s
✓ paginasi menyertakan meta dan links 0.02s
✓ abilities membedakan token baca dan tulis 0.01s
✓ validasi 422 dengan envelope error 0.01s
✓ header accept mengubah bentuk penolakan tamu 0.01s
✓ 404 menggunakan envelope error kustom 0.01s
✓ patch dan delete mengelola satu post 0.01s
Tests: 8 passed (50 assertions)
Duration: 0.33sEndpoint API kita juga menerima file — profil dengan foto, dokumen PDF. Upload file butuh perlakuan khusus di test, dan Laravel menyediakan UploadedFile::fake() beserta fake storage untuk itu. Chapter File Upload Testing
berikutnya membedah keduanya. Rujukan lengkap ada di dokumentasi HTTP Tests
.