Most Livewire components I see in code review have zero tests. The usual excuse is that testing a reactive component means spinning up a browser: slow, flaky, and not worth the effort. That excuse does not hold in Livewire 4. Livewire::test() mounts a component in memory as a plain Laravel feature test, so you can set properties, call actions, and assert on both internal state and rendered HTML in milliseconds. Pest is the recommended framework for this, and the whole testing API is small enough to learn in one sitting.
Scaffold a Pest test for the component#
Livewire 4 recommends Pest as the testing framework, and it can generate a test file next to your component so you never start from a blank page. Append the --test flag to make:livewire and Livewire scaffolds a matching test. For a view-based (single-file or multi-file) component the test lands beside the component in resources/views; for a class-based component you get a PHPUnit-style test under tests/Feature/Livewire, which you can keep as-is or convert to Pest. Add the RefreshDatabase trait at the top so each test runs against a clean database.
# Generate a component and its test in one go
php artisan make:livewire post.create --test
<?php
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class); // Reset the DB between every test
it('renders successfully', function () {
Livewire::test('post.create')
->assertStatus(200);
});
Mount the component and assert its initial render#
The cheapest test you can write mounts the component and checks it renders without blowing up. Livewire::test() accepts either the component name ('post.create') or its class, and assertStatus(200) plus assertSee() give you a solid smoke test. When a component receives data from a parent or a route, pass that data as the second argument, exactly like Livewire calls mount() at runtime, then assert the properties hydrated correctly.
it('smoke tests the create form', function () {
Livewire::test('post.create')
->assertStatus(200)
->assertSee('Publish');
});
it('populates the title when editing an existing post', function () {
$post = Post::factory()->create(['title' => 'Existing title']);
// Second argument is passed straight into mount()
Livewire::test('post.edit', ['post' => $post])
->assertSet('title', 'Existing title');
});
Set properties and call actions#
This is the core loop of every Livewire test: drive the component the way a user would. set() updates a property (pass an array to set several at once), call() invokes an action by name, and both return the test instance so you can chain the whole interaction into one readable statement. Parameters passed to call() land on the action method, and toggle() is a handy shortcut for flipping a boolean.
it('creates a post when the form is submitted', function () {
expect(Post::count())->toBe(0);
Livewire::test('post.create')
->set('title', 'My new post')
->set('content', 'Post content here')
->call('save'); // Invoke the save() action
expect(Post::count())->toBe(1);
});
it('deletes a post by id', function () {
$post = Post::factory()->create();
Livewire::test('post.show', ['post' => $post])
->call('delete', $post->id); // Parameters flow into the action
});
Assert updated state and rendered output#
Here is the distinction that trips up newcomers: assertSet() inspects a PHP property on the component, while assertSee() inspects the rendered HTML. State and output are different questions, and a good test often checks both. Reach for assertViewHas() when you care about the data handed to the view rather than the exact markup, and use withQueryParams() for components that keep state in the query string. If your component syncs filters to the URL, my guide to binding Livewire state to the query string covers the runtime side of the same feature you are now testing.
it('updates state and output together', function () {
Livewire::test('counter')
->call('increment')
->assertSet('count', 1) // Internal property value
->assertSee('Count: 1'); // Rendered HTML
});
it('filters posts from a url query string', function () {
Post::factory()->create(['title' => 'Laravel testing']);
Post::factory()->create(['title' => 'Vue components']);
Livewire::withQueryParams(['search' => 'Laravel'])
->test('search-posts')
->assertSee('Laravel testing')
->assertDontSee('Vue components');
});
Test validation errors and dispatched events#
Validation and events are where component tests earn their keep. assertHasErrors('title') confirms a field failed validation, and passing an array lets you assert the exact rule that fired. For events, assertDispatched() verifies the component emitted an event, and you can match on the event's parameters too. If you have moved rules out of the component and into a form object, the same assertions still apply, that pattern is worth adopting and I cover it in organising validation with Livewire 4 form objects. Note that these tests never touch a browser; for full click-through flows you want Pest 4 browser testing with Playwright and Livewire::visit() instead.
it('requires a title', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors(['title' => ['required']]); // Assert the exact rule
});
it('dispatches an event after saving', function () {
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertDispatched('post-created');
});
it('dispatches a notification with the right message', function () {
$post = Post::factory()->create();
Livewire::test('post.show', ['post' => $post])
->call('delete', postId: $post->id)
->assertDispatched('notify', message: 'Post deleted'); // Match on params
});
Run the test with composer test#
Because these are ordinary feature tests, they run with the rest of your suite. On this project I run composer test, which wraps php artisan test and fails the build if a single assertion breaks; while iterating on one component, filter down to it so the feedback loop stays tight. From here, grow the suite by testing the flows that actually break in production, multi-step forms are a common culprit, and validating a multi-step wizard per step shows exactly what those assertions look like. Once the tests exist, enforce that new code stays covered with type coverage in Pest.
# Run the whole suite
composer test
# Or focus on one component while iterating
php artisan test --filter=PostCreate
FAQ#
How do I test a Livewire component with Pest?
Call Livewire::test('component-name') inside a Pest it() block, then chain set() to update properties and call() to trigger actions. Finish with assertions like assertSet(), assertSee(), or assertHasErrors(). It runs as a fast feature test, so add uses(RefreshDatabase::class) at the top of the file if the component touches the database.
What is the difference between assertSee and assertSet?
assertSet() checks the value of a PHP property on the component, which is its internal state. assertSee() checks the rendered HTML output, which is what the user would actually see on the page. A property can be set correctly while the template fails to display it, so testing both catches different classes of bug.
How do I test Livewire validation errors?
Set a property to an invalid value, call the action that validates, then assert with assertHasErrors('field'). To confirm a specific rule fired rather than just any error, pass an array such as assertHasErrors(['title' => ['required', 'min:3']]). Use assertHasNoErrors() to prove that valid input passes validation cleanly.
How do I test events dispatched by a Livewire component?
Use assertDispatched('event-name') after calling the action that emits the event. To check the payload, pass the expected parameters, for example assertDispatched('notify', message: 'Post deleted'), or supply a closure for more complex assertions. assertNotDispatched() verifies an event was deliberately not fired.
Do I need a browser to test Livewire components?
No. Livewire::test() mounts the component in memory and runs as a standard Laravel feature test with no browser involved, which is why it is fast enough to run on every change. Only reach for a real browser, via Livewire::visit() and the Pest 4 browser plugin, when you need to test JavaScript-heavy interactions or genuine end-to-end user flows.