Menguji komponen Livewire secara otomatis: Livewire::test untuk memanggil action dan mengeset property, assertion assertSee, assertHasErrors, dan assertDispatched, hingga test CRUD, validasi, dan alur form lengkap.

Aplikasi yang tumbuh tanpa test adalah bom waktu — refactor yang tampaknya aman bisa menghancurkan fitur yang tidak terpikirkan. Keunggulan arsitektur Livewire adalah komponennya mudah diuji: karena state dan logika hidup di PHP, kalian bisa "memainkan" komponen sepenuhnya di dalam test, tanpa browser.
Di episode ini kita memakai Livewire::test() — API test bawaan yang menginstansiasi komponen, memanggil action, mengeset property, dan mengecek output serta perilaku. Contoh memakai sintaks Pest, yang lebih ringkas, tapi konsepnya sama persis di PHPUnit.
Install Pest bila belum ada:
composer require pestphp/pest --dev
php artisan pest:installuse Livewire\Livewire;
use App\Livewire\Counter;
it('menambah count saat tombol diklik', function () {
Livewire::test(Counter::class)
->call('increment')
->assertSee('1');
});Livewire::test(Counter::class) — instansiasi komponen dalam test harness.call('increment') — jalankan action, sama seperti klik dari view.assertSee('1') — pastikan output berisi "1".Jalankan:
php artisan test| Assertion | Kegunaan |
|---|---|
assertSee('teks') | Output mengandung teks |
assertSet('property', 'nilai') | Property berisi nilai tertentu |
assertHasErrors(['title']) | Field punya error validasi |
assertDispatched('event') | Event terkirim |
assertRedirect('/path') | Aksi me-redirect |
assertViewIs('view.name') | View yang dirender |
Contoh kombinasi untuk form:
use Livewire\Livewire;
use App\Livewire\CreatePost;
it('memvalidasi judul wajib diisi', function () {
Livewire::test(CreatePost::class)
->set('title', '')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('menyimpan post yang valid', function () {
Livewire::test(CreatePost::class)
->set('title', 'Post pertama')
->set('body', 'Isi yang cukup panjang untuk lolos validasi.')
->call('save')
->assertHasNoErrors()
->assertDispatched('post-created')
->assertSet('title', '');
$this->assertDatabaseHas('posts', [
'title' => 'Post pertama',
]);
});Perhatikan alur test-nya meniru user: set field → panggil action → cek efek. set() menggantikan wire:model, call() menggantikan wire:click/wire:submit.
Tip
Saat memakai Form Objects, alur yang sama berjalan: ->set('form.title', '...'). Assertion error juga ikut namespace field, misalnya assertHasErrors(['form.title']). Test tidak membedakan — state form tetap diuji seperti property biasa.
use App\Livewire\PostsIndex;
use App\Models\Post;
it('menghapus post yang dipilih', function () {
$post = Post::factory()->create();
Livewire::test(PostsIndex::class)
->call('delete', $post->id)
->assertSet('posts', []);
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
});Test memanggil action dengan argumen persis seperti wire:click="delete({{ $post->id }})".
it('mengirim event post-created', function () {
Livewire::test(CreatePost::class)
->set('title', 'Judul baru')
->set('body', 'Isi konten yang valid.')
->call('save')
->assertDispatched('post-created');
});
it('mendengarkan event dan memperbarui statistik', function () {
Livewire::test(StatsPanel::class)
->dispatch('post-created')
->assertSee('1');
});dispatch('event') di sisi test mensimulasikan event yang datang dari komponen lain — tepat untuk menguji listener tanpa perlu memainkan pengirim aslinya.
Component yang memakai authorize() butuh user login:
it('menolak user non-admin menghapus post', function () {
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->actingAs($user);
Livewire::test(PostActions::class)
->call('delete', $post->id)
->assertForbidden();
});
it('mengizinkan admin menghapus post', function () {
$admin = User::factory()->admin()->create();
$post = Post::factory()->create();
$this->actingAs($admin);
Livewire::test(PostActions::class)
->call('delete', $post->id)
->assertHasNoErrors();
});actingAs($user) menyetel user aktif; policy diuji sungguhan — bukan hanya bentuknya.
use App\Livewire\PostEditor;
use App\Models\Post;
it('mengedit post yang sudah ada', function () {
$post = Post::factory()->create([
'title' => 'Judul lama',
'body' => 'Konten lama.',
]);
Livewire::test(PostEditor::class, ['post' => $post])
->assertSet('form.title', 'Judul lama')
->set('form.title', 'Judul baru')
->call('save')
->assertRedirect(route('posts.index'));
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'title' => 'Judul baru',
]);
});Perhatikan ['post' => $post] — argumen kedua Livewire::test() dikirim ke mount(), persis seperti <livewire:post-editor :post="$post" /> di view.
Livewire::test() tidak menjalankan JavaScript. Untuk interaksi yang murni client-side (dropdown Alpine, transisi), gunakan Laravel Dusk — test browser sungguhan:
composer require laravel/dusk --dev
php artisan dusk:installuse Laravel\Dusk\Browser;
it('menampilkan modal konfirmasi', function () {
$this->browse(function (Browser $browser) {
$browser->visit('/posts')
->click('@delete-button')
->waitForText('Yakin ingin menghapus?')
->press('Ya, hapus')
->waitForText('Post dihapus');
});
});Prinsipnya: Livewire::test untuk logika & data; Dusk untuk behavior visual. Keduanya saling melengkapi, tidak menggantikan.
actingAs untuk komponen ber-auth: otorisasi gagal tak terduga. Set user sejak awal.assertSee memeriksa teks persis: output Blade bisa mengandung markup tambahan; sesuaikan dengan teks yang benar-benar dirender.Model::factory() dan migrasi dijalankan (biasanya otomatis di RefreshDatabase).Inti yang harus dibawa pulang:
Livewire::test(Component::class) menguji komponen tanpa browser; call(), set(), dispatch() memainkannya.assertSee, assertSet, assertHasErrors, assertDispatched, assertRedirect, assertForbidden.actingAs; test Form Objects lewat namespace form.*.Di episode 21 selanjutnya kita akan membahas performance & production optimization — query efficiency, caching dengan #[Computed], lazy loading, route cache & OPcache, serta cara mengukur payload dan DOM diff sebelum deploy. Sampai jumpa di episode 21!