
You deployed on Friday. It broke. Users complained. You spent the weekend firefighting.
Sound familiar? Manual testing is a trap — it feels fast until it costs you sleep. The solution is automated tests, and with Pest + Livewire 3, writing them has never been easier.
Why Test Livewire Components?
Livewire components are deceptively complex. They handle:
- User interactions (clicks, form submissions)
- Real-time validation
- State management across requests
- Authorization and visibility logic
A single untested component can break in dozens of ways. Pest gives you confidence that your component works before production traffic hits it.
Setting Up Pest for Livewire
If you're not already using Pest, install it alongside the Livewire plugin:
1composer require pestphp/pest --dev
2composer require pestphp/pest-plugin-livewire --dev
3./vendor/bin/pest --init
Pest's Livewire plugin gives you a fluent, expressive API that feels natural to write and read.
Your First Livewire Test
Let's say you have a CreatePost component with a form. Here's how to test it:
1use App\Livewire\CreatePost;
2use Livewire\Livewire;
3
4it('can create a post', function () {
5 Livewire::test(CreatePost::class)
6 ->set('title', 'My First Post')
7 ->set('body', 'This is the body content')
8 ->call('save')
9 ->assertHasNoErrors();
10
11 expect(Post::count())->toBe(1);
12});
Clean, readable, and catches bugs before users do.
Testing Validation Rules
Validation is where Livewire shines — and where tests matter most. Here's how to verify your rules work:
1it('requires a title', function () {
2 Livewire::test(CreatePost::class)
3 ->set('title', '')
4 ->set('body', 'Valid body')
5 ->call('save')
6 ->assertHasErrors(['title' => 'required']);
7});
8
9it('validates title length', function () {
10 Livewire::test(CreatePost::class)
11 ->set('title', str_repeat('x', 256))
12 ->call('save')
13 ->assertHasErrors(['title' => 'max']);
14});
Test every validation rule. Users will find edge cases you didn't think of.
Testing Component State
Livewire components maintain state across wire requests. Test that state updates correctly:
1it('updates preview when title changes', function () {
2 Livewire::test(CreatePost::class)
3 ->set('title', 'My New Title')
4 ->assertSet('preview', 'my-new-title');
5});
6
7it('resets form after save', function () {
8 Livewire::test(CreatePost::class)
9 ->set('title', 'Post Title')
10 ->set('body', 'Post Body')
11 ->call('save')
12 ->assertSet('title', '')
13 ->assertSet('body', '')
14 ->assertDispatched('post-created');
15});
Testing Authorization
Never trust the frontend. Test that your authorization logic actually works:
1it('prevents guests from creating posts', function () {
2 Livewire::test(CreatePost::class)
3 ->call('save')
4 ->assertForbidden();
5});
6
7it('allows authenticated users to create posts', function () {
8 $user = User::factory()->create();
9
10 Livewire::actingAs($user)
11 ->test(CreatePost::class)
12 ->set('title', 'My Post')
13 ->set('body', 'My Body')
14 ->call('save')
15 ->assertHasNoErrors();
16});
The actingAs() method makes auth testing seamless.
Testing Events and Navigation
Livewire 3 uses events for component communication. Test them:
1it('dispatches event when post is created', function () {
2 Livewire::actingAs(User::factory()->create())
3 ->test(CreatePost::class)
4 ->set('title', 'Test')
5 ->set('body', 'Body')
6 ->call('save')
7 ->assertDispatched('post-created');
8});
9
10it('redirects to post after creation', function () {
11 Livewire::actingAs(User::factory()->create())
12 ->test(CreatePost::class)
13 ->set('title', 'Test')
14 ->set('body', 'Body')
15 ->call('save')
16 ->assertRedirect('/posts/1');
17});
Testing Wire:Model Updates
Livewire 3 defers updates by default. Test real-time validation with live models:
1it('shows validation errors in real-time', function () {
2 Livewire::test(CreatePost::class)
3 ->set('title', 'ab') // Too short
4 ->assertHasErrors(['title' => 'min']);
5});
Advanced: Testing File Uploads
Livewire handles file uploads gracefully. Test them with UploadedFile:
1use Illuminate\Http\UploadedFile;
2use Illuminate\Support\Facades\Storage;
3
4it('can upload a featured image', function () {
5 Storage::fake('public');
6
7 $file = UploadedFile::fake()->image('photo.jpg');
8
9 Livewire::actingAs(User::factory()->create())
10 ->test(CreatePost::class)
11 ->set('title', 'Post with Image')
12 ->set('body', 'Body')
13 ->set('image', $file)
14 ->call('save')
15 ->assertHasNoErrors();
16
17 Storage::disk('public')
18 ->assertExists('posts/' . $file->hashName());
19});
The Test Coverage Sweet Spot
You don't need 100% coverage. Focus on:
- Happy paths — The main user flows that must work
- Validation — Every rule, every edge case
- Authorization — Who can do what
- State changes — Data that updates other data
- Events — Cross-component communication
Skip testing Livewire internals (it's already tested). Focus on your logic.
Running Your Tests
1./vendor/bin/pest --filter=Livewire
Or run everything:
1./vendor/bin/pest
Add it to your CI pipeline and never deploy broken code again.
Key Takeaways
- Pest + Livewire makes testing delightful, not painful
- Test validation, authorization, and state — the things that break
- Use
actingAs() for auth, assertDispatched() for events
- Deploy on Friday with confidence
Your users (and your weekends) will thank you.
A practical guide from the Beartropy team