Anda menambah fitur diskon di aplikasi kasir, mengetesnya di browser, semuanya beres. Dua hari kemudian pelanggan melapor bahwa total belanja tanpa diskon ikut salah hitung. Fitur lama rusak diam-diam karena tidak ada yang mengeceknya ulang. Inilah masalah yang diselesaikan testing otomatis.
Laravel sudah membawa semua perkakas testing sejak instalasi, jadi tidak ada alasan menundanya. Artikel ini membahas cara menulis test pertama, menguji database dengan aman, memalsukan email dan API eksternal, serta error yang paling sering membuat pemula menyerah.
Kenapa Test Layak Ditulis
- Mencegah regresi. Satu perintah memeriksa ulang puluhan skenario dalam hitungan detik.
- Dokumentasi yang selalu benar. Test menjelaskan perilaku yang diharapkan, dan langsung gagal jika perilaku itu berubah.
- Berani merapikan kode. Refactor tidak lagi menakutkan karena ada jaring pengaman.
Biayanya nyata: menulis test menambah waktu di awal. Karena itu, mulailah dari fitur yang paling mahal jika rusak, misalnya login, checkout, dan perhitungan uang, bukan dari halaman "Tentang Kami".
Feature Test vs Unit Test
| Aspek | Feature test | Unit test |
|---|---|---|
| Folder | tests/Feature | tests/Unit |
| Yang diuji | Request HTTP sampai respons dan isi database | Satu class atau method secara terisolasi |
| Akses framework | Penuh (routing, database, session) | Tidak ada; murni PHP |
| Kecepatan | Lebih lambat | Sangat cepat |
| Cocok untuk | Alur pengguna: simpan produk, login, checkout | Logika murni: kalkulator diskon, format nomor faktur |
Untuk pemula, feature test memberi manfaat paling besar karena menguji hal yang benar-benar dialami pengguna.
Langkah 1: Siapkan Database Khusus Test
Jangan pernah menjalankan test terhadap database yang berisi data asli. Buka phpunit.xml dan aktifkan dua baris yang secara default masih berupa komentar:
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
SQLite di memori dibuat ulang setiap kali test berjalan, cepat, dan tidak menyentuh MySQL Anda. Jika aplikasi memakai fitur khusus MySQL (misalnya fungsi FIELD() atau fulltext), buat database MySQL terpisah bernama misalnya toko_testing dan arahkan ke sana.
Langkah 2: Buat File Test
php artisan make:test ProductTest # feature test
php artisan make:test DiscountTest --unit # unit test
Installer Laravel 11 menawarkan pilihan Pest atau PHPUnit saat membuat project. Keduanya memakai mesin yang sama; Pest hanya menyediakan sintaks yang lebih ringkas. Contoh di bawah memakai PHPUnit karena gayanya lebih dikenal, diikuti versi Pest untuk perbandingan.
Langkah 3: Test Pertama
namespace Tests\Feature;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProductTest extends TestCase
{
use RefreshDatabase;
public function test_tamu_dialihkan_ke_login(): void
{
$this->get('/products')->assertRedirect('/login');
}
public function test_user_bisa_menambah_produk(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/products', [
'name' => 'Kopi Susu',
'price' => 15000,
'stock' => 10,
]);
$response->assertRedirect('/products');
$this->assertDatabaseHas('products', ['name' => 'Kopi Susu', 'price' => 15000]);
}
public function test_harga_negatif_ditolak(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->post('/products', ['name' => 'Teh', 'price' => -1, 'stock' => 1])
->assertSessionHasErrors('price');
$this->assertDatabaseCount('products', 0);
}
}
Ada tiga pola penting di sini. RefreshDatabase menjalankan migrasi dan membersihkan data di setiap test, sehingga urutan test tidak saling memengaruhi. actingAs() login sebagai user tanpa melewati form. Dan test ketiga menguji jalur gagal, bagian yang paling sering terlupa padahal paling sering dimanfaatkan input nakal.
Versi Pest untuk test kedua terlihat seperti ini:
use App\Models\User;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
test('user bisa menambah produk', function () {
$user = User::factory()->create();
$this->actingAs($user)
->post('/products', ['name' => 'Kopi Susu', 'price' => 15000, 'stock' => 10])
->assertRedirect('/products');
expect(\App\Models\Product::count())->toBe(1);
});
Pola Arrange, Act, Assert
Perhatikan bahwa setiap test di atas punya tiga bagian. Arrange: siapkan kondisi, misalnya membuat user lewat factory. Act: lakukan satu aksi, misalnya mengirim POST. Assert: periksa hasilnya. Pisahkan ketiganya dengan baris kosong. Jika satu test mulai berisi beberapa aksi berbeda, itu tanda test tersebut sebaiknya dipecah menjadi dua agar pesan gagalnya jelas menunjuk penyebab.
Langkah 4: Unit Test untuk Logika Murni
Misalkan perhitungan diskon dipisah ke class App\Support\Discount dengan method statis apply(int $total, int $percent): int. Unit test-nya tidak butuh database sama sekali:
namespace Tests\Unit;
use App\Support\Discount;
use PHPUnit\Framework\TestCase;
class DiscountTest extends TestCase
{
public function test_diskon_sepuluh_persen(): void
{
$this->assertSame(90000, Discount::apply(100000, 10));
}
public function test_diskon_tidak_membuat_total_negatif(): void
{
$this->assertSame(0, Discount::apply(50000, 150));
}
}
Perhatikan bahwa unit test mewarisi PHPUnit\Framework\TestCase, bukan Tests\TestCase, sehingga framework tidak di-boot dan test berjalan sangat cepat. Memisahkan logika seperti ini juga membuat kode lebih mudah dipahami.
Langkah 5: Palsukan Email, Antrean, dan API Luar
Test tidak boleh mengirim email sungguhan atau memanggil payment gateway asli. Laravel menyediakan fake untuk itu:
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
Mail::fake();
Http::fake([
'api.contoh-ongkir.test/*' => Http::response(['cost' => 12000], 200),
]);
// ... jalankan request checkout ...
Mail::assertSent(\App\Mail\InvoiceMail::class);
Http::assertSentCount(1);
Nama domain dan class mail di atas hanyalah contoh. Polanya sama untuk Queue::fake(), Notification::fake(), dan Storage::fake().
Menjalankan Test
php artisan test # semua test
php artisan test --filter=ProductTest # satu file atau nama test
php artisan test --parallel # paralel, perlu paket brianium/paratest
Menentukan Apa yang Diuji Lebih Dulu
Pada project yang sudah berjalan tanpa test, menulis test untuk semuanya sekaligus tidak realistis. Gunakan urutan prioritas berikut:
- Uang. Perhitungan total, diskon, pajak, dan saldo. Kesalahan di sini langsung merugikan.
- Akses. Halaman admin tidak bisa dibuka user biasa, data satu pelanggan tidak terlihat oleh pelanggan lain.
- Alur utama. Daftar, login, membuat transaksi, mencetak faktur.
- Bug yang pernah terjadi. Setiap bug yang diperbaiki mendapat satu test agar tidak muncul lagi.
- Sisanya ditambah bertahap setiap kali Anda menyentuh fitur tersebut.
Jangan mengejar angka cakupan (coverage) seratus persen. Sepuluh test yang menjaga alur penting jauh lebih berharga daripada ratusan test yang hanya memeriksa halaman statis.
Menjalankan Test Otomatis di Setiap Push
Test paling berguna ketika berjalan tanpa perlu diingat. Jika repositori Anda di GitHub, buat file .github/workflows/tests.yml:
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist
- run: cp .env.example .env && php artisan key:generate
- run: php artisan test
Setiap push dan pull request kini otomatis dites, dan tanda silang merah muncul sebelum kode bermasalah sampai ke server.
Error Umum dan Perbaikannya
| Error | Penyebab | Perbaikan |
|---|---|---|
| no such table: products | Lupa RefreshDatabase | Tambahkan trait di class test |
| Call to undefined method factory() | Model belum memakai HasFactory | Tambahkan trait di model |
| Vite manifest not found | Asset frontend belum di-build | Panggil $this->withoutVite() atau jalankan npm run build |
| Data asli hilang setelah test | Test memakai database utama | Atur koneksi testing di phpunit.xml |
| Test lolos sendiri, gagal bersama | Test saling bergantung pada data | Buat data di dalam masing-masing test |
Checklist Kebiasaan Testing
- Setiap bug yang ditemukan dibuatkan test sebelum diperbaiki.
- Setiap fitur punya minimal satu test jalur sukses dan satu jalur gagal.
- Test tidak pernah menyentuh database produksi atau layanan berbayar.
php artisan testdijalankan sebelum commit dan sebelum deploy.- Nama test menjelaskan perilaku, bukan nama method.
Jika Anda baru belajar membangun fitur, mulailah dari CRUD Laravel 11 dari nol, lalu tambahkan test untuk setiap aksinya. Test juga sangat membantu saat mengkustomisasi source code tanpa merusak, dan sebaiknya menjadi langkah wajib sebelum Anda deploy aplikasi Laravel.