Feature test di Laravel mengirim request sungguhan ke app-mu, tanpa browser dan tanpa server yang menyala, lalu memeriksa jawabannya dan isi database-nya. Bayangin uji coba sebelum perpustakaan buka. Petugas pura-pura jadi admin, mendaftarkan buku Ronggeng Dukuh Paruk di loket, lalu memeriksa dua hal: loketnya menjawab "tercatat", dan bukunya benar-benar ada di rak. Kalau loketnya bilang tercatat tapi raknya kosong, ada yang salah, dan lebih baik ketahuan sekarang daripada saat anggota datang.
🧪 Tebakan cepat:
storediBookControllermenjawab denganreturn response()->json($book);, tanpa angka status. Test-nya memeriksaassertStatus(201). Test itu lulus atau gagal, dan kalau gagal, apa yang dicetak? Tebak dulu, jawabannya ada di bagian "Menjalankan: php artisan test".
Feature test dan unit test
| Feature test | Unit test | |
|---|---|---|
| Menguji | satu request utuh: route, middleware, controller, database | satu class atau function saja |
| Folder | tests/Feature |
tests/Unit |
| Dibuat dengan | php artisan make:test BookTest |
php artisan make:test BookTest --unit |
Dasar PHPUnit (class ...Test, method test_..., assertion) sudah dibahas di pelajaran PHPUnit Dasar. Waktu membuat project, Laravel juga menawarkan Pest, yang menulis test tanpa class (pelajaran Testing with Pest). Di sini kita pakai PHPUnit 12, gaya yang dipakai di latihan.
Test pertama: POST /api/books
Yang diuji adalah store milik BookController, di balik route POST /api/books:
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'author' => 'required|string',
'stock' => 'required|integer|min:0',
]);
$book = Book::create($validated);
return response()->json($book, 201);
}
Test-nya, di tests/Feature/BookTest.php:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BookTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_book(): void
{
$response = $this->postJson('/api/books', [
'title' => 'Ronggeng Dukuh Paruk',
'author' => 'Ahmad Tohari',
'stock' => 1,
]);
$response->assertStatus(201);
$this->assertDatabaseHas('books', ['title' => 'Ronggeng Dukuh Paruk']);
}
public function test_validation_fails_without_title(): void
{
$response = $this->postJson('/api/books', [
'author' => 'Ahmad Tohari',
'stock' => 1,
]);
$response->assertStatus(422);
}
}
use RefreshDatabase;menjalankan migration sebelum test, dan tiap test mulai dari database kosong (dibahas di pelajaran Testing Models & Database).- Nama method harus diawali
test_. Method lain gak dijalankan. $this->postJson('/api/books', [...])mengirimPOSTdengan body JSON dan headerAccept: application/json, jadi validasi yang gagal dijawab422, bukan redirect. UntukGETada$this->getJson('/api/books').$response->assertStatus(201)memeriksa kode status.assertOk()adalah singkatan untukassertStatus(200).$this->assertDatabaseHas('books', ['title' => 'Ronggeng Dukuh Paruk'])memeriksa ada baris dengan judul itu di tabelbooks. Status201saja belum membuktikan bukunya tersimpan.- Test kedua mengirim request kedua, tanpa
title, dan mengharap422.
Menjalankan: php artisan test
PASS Tests\Feature\BookTest
✓ can create book 0.12s
✓ validation fails without title 0.01s
Tests: 2 passed (3 assertions)
Duration: 0.21s
Tiap ✓ satu test yang lulus, dan namanya ditulis tanpa test_ dan garis bawah. Tiga assertion: dua di test pertama, satu di test kedua. Sekarang ubah jawaban store jadi return response()->json($book);:
FAIL Tests\Feature\BookTest
⨯ can create book 0.12s
✓ validation fails without title 0.01s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\BookTest > can create book
Expected response status code [201] but received 200.
Failed asserting that 200 is identical to 201.
at tests/Feature/BookTest.php:20
➜ 20▕ $response->assertStatus(201);
Gagal. Tanpa angka status, response()->json(...) menjawab 200, dan assertStatus(201) minta persis 201. Itu jawaban tebakannya. ⨯ menandai test yang gagal, dan ➜ menunjuk baris assertion yang berhenti. Kalau store lupa Book::create, test pertama gagal di baris 21: Failed asserting that a row in the table [books] matches the attributes {"title": "Ronggeng Dukuh Paruk"}. The table is empty.
📌 Login dan dunia luar
Route di balik middleware auth diuji dengan $this->actingAs($user)->postJson(...). Kirim email, panggil API luar, atau masukkan job ke antrean gak boleh terjadi sungguhan di test: panggil Mail::fake(), Http::fake(), atau Queue::fake() di awal test, lalu periksa dengan Mail::assertSent(...), Http::assertSent(...), atau Queue::assertPushed(...). Untuk service milikmu sendiri, serahkan versi palsunya lewat container: $this->app->instance(NotificationService::class, new FakeNotification()).
Di latihan
Latihannya menguji POST /api/products dengan bentuk yang sama: test_can_create_product mengirim "name" => "Mouse" dan "price" => 150000, lalu assertStatus(201) dan assertDatabaseHas("products", ["name" => "Mouse"]); test_validation_fails_without_name mengirim request kedua tanpa name, lalu assertStatus(422). Route POST /api/products-nya sudah ada di kode awal, dan kedua test dijalankan seperti php artisan test, jadi kalau jawabanmu benar output-nya memuat ✓ can create product, ✓ validation fails without name, dan Tests: 2 passed.
⚠️ Jebakan yang sering ketemu
- Satu request diperiksa dua kali. Status
201dan422datang dari dua keadaan, jadi butuh duapostJson, satu dengan data lengkap dan satu tanpatitle. - Lupa awalan
test_.public function validation_fails_without_title()gak dijalankan sama sekali. Hasilnya tetap hijau, cuma1 passed, dan kesalahannya gak pernah ketahuan. - Teks yang beda sedikit.
assertDatabaseHas('books', ['title' => 'ronggeng dukuh paruk'])gagal walau bukunya ada:Found: [{"title": "Ronggeng Dukuh Paruk"}]. Huruf besar kecil ikut dibandingkan. - Lupa
use RefreshDatabase;. Migration gak dijalankan untuk test, jadi dengan database:memory:tabelnya gak ada:received 500karenano such table: books.
TL;DR: Feature test ada di tests/Feature (php artisan make:test BookTest), unit test di tests/Unit (--unit). Class-nya extends TestCase, pakai use RefreshDatabase;, dan tiap method diawali test_. $this->postJson('/api/books', [...]) atau $this->getJson(...) mengirim request sungguhan, $response->assertStatus(201) (atau assertOk() untuk 200) memeriksa status, dan $this->assertDatabaseHas('books', [...]) memeriksa barisnya tersimpan. Satu keadaan, satu request. php artisan test mencetak ✓ untuk yang lulus, ⨯ plus pesan seperti Expected response status code [201] but received 200. dan baris yang ditunjuk ➜ untuk yang gagal.