Testing: Feature Test untuk Route
Tulis test otomatis: GET sukses, POST validasi, auth proteksi.
Kenapa Test Itu Investasi, Bukan Beban
Setiap developer pernah merasakannya: mengubah satu baris "sepele", lalu tiga fitur lain rusak diam-diam dan ketahuan seminggu kemudian oleh user. Test otomatis adalah jaring pengaman yang menangkap kerusakan itu dalam hitungan detik, bukan minggu. Manfaat terbesarnya bukan menemukan bug hari ini, tapi memberi keberanian untuk refactor bulan depan. Kode tanpa test seperti berjalan di atas tali tanpa jaring: semakin besar aplikasi, semakin menakutkan setiap perubahan.
Feature Test: Uji Alur Lengkap
Feature test mensimulasikan request HTTP sungguhan ke aplikasimu, dari route sampai database:
php artisan make:test PostTest// tests/Feature/PostTest.php
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase; // migrasi fresh di DB testing tiap test
public function test_guest_tidak_bisa_buat_post(): void
{
$response = $this->post('/posts', ['title' => 'x', 'body' => 'y']);
$response->assertRedirect('/login');
}
public function test_user_bisa_buat_post_valid(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => 'Judul Asli',
'body' => 'Isi post yang cukup panjang...',
]);
$response->assertRedirect('/posts');
$this->assertDatabaseHas('posts', [
'title' => 'Judul Asli',
'user_id' => $user->id,
]);
}
public function test_validasi_menolak_judul_kosong(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => '',
'body' => 'Isi...',
]);
$response->assertSessionHasErrors('title');
$this->assertDatabaseCount('posts', 0);
}
public function test_user_tidak_bisa_hapus_post_orang_lain(): void
{
$pemilik = User::factory()->create();
$penyusup = User::factory()->create();
$post = Post::factory()->create(['user_id' => $pemilik->id]);
$response = $this->actingAs($penyusup)->delete("/posts/{$post->id}");
$response->assertForbidden();
$this->assertDatabaseHas('posts', ['id' => $post->id]);
}
}Empat test ini mengunci perilaku kritis: auth, happy path, validasi, dan otorisasi. Kalau suatu hari ada yang "iseng" menghapus middleware auth, test pertama langsung merah.
Unit Test: Uji Logika Murni
Untuk logika bisnis tanpa HTTP (hitung diskon, format invoice), unit test lebih cepat dan fokus:
// tests/Unit/DiskonTest.php
namespace Tests\Unit;
use App\Services\DiskonService;
use PHPUnit\Framework\TestCase;
class DiskonTest extends TestCase
{
public function test_member_gold_dapat_diskon_20_persen(): void
{
$service = new DiskonService();
$this->assertEquals(80000, $service->hitung(100000, 'gold'));
}
public function test_diskon_tidak_melebihi_batas_maksimal(): void
{
$service = new DiskonService();
$this->assertEquals(50000, $service->hitung(1000000, 'platinum'));
}
}Fake: Jangan Sentuh Dunia Nyata di Test
Test tidak boleh mengirim email beneran atau menulis ke disk production. Laravel menyediakan fake untuk semuanya:
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
public function test_registrasi_mengirim_email_verifikasi(): void
{
Mail::fake();
Storage::fake('public');
$this->post('/register', [...]);
Mail::assertSent(VerifyEmail::class);
}
public function test_import_besar_masuk_antrean(): void
{
Queue::fake();
$this->actingAs($admin)->post('/import', [...]);
Queue::assertPushed(ProsesImport::class);
}Setup Database Testing
Jangan jalankan test di database development! Di phpunit.xml, arahkan ke SQLite in-memory yang super cepat:
<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_MAILER" value="array"/>
</php>Lalu php artisan test menjalankan semuanya dengan output rapi per test.
Jebakan Umum
Test yang bergantung urutan. Setiap test harus independen: RefreshDatabase memastikan tidak ada sisa data dari test sebelumnya. Jangan pernah mengasumsikan test A jalan sebelum test B.
Terlalu banyak mock. Mock berguna untuk API eksternal, tapi me-mock model Eloquent sendiri biasanya tanda desain yang salah. Pakai database testing asli, itu cepat kok.
Hanya test happy path. Bug hidup di jalur gagal: validasi, 403, 404, input aneh. Aturan praktis: untuk setiap test sukses, tulis minimal satu test kegagalan.
Test lambat lalu tidak dijalankan. Kalau suite 10 menit, tidak ada yang menjalankannya. Jaga test cepat (SQLite, fake semua I/O eksternal) agar php artisan test jadi kebiasaan, bukan ritual tahunan.
Catatan teknis: Laravel 11 memakai Pest sebagai opsi default di installer baru (
php artisan testtetap jalan untuk PHPUnit). Sintaks Pest lebih ringkas (test('...', fn () => ...)), tapi konsepnya identik. Pilih satu dan konsisten di seluruh project, jangan campur aduk.
Tantangan
Test Policy
Tulis test: user A membuat post, user B mencoba update via PUT -> 403, user A update -> 200/302. Jalankan php artisan test dan pastikan hijau.
Kuis Bab
Uji pemahamanmu: Bab 9: Fitur Lanjutan
Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".
1.Kapan memakai Mail vs Notification?
2.Kenapa kirim email sebaiknya via queue?
3.Apa syarat job yang baik terkait retry?
4.Apa risiko cache yang tidak pernah di-invalidate?
5.Apa fungsi trait RefreshDatabase di test?