How do you test a Livewire newsletter sign-up component with Livewire::test(), set(), call() and assertSet()?
answer
- Livewire::test() renders without a browser
- second argument goes to mount()
- each set() and call() is a round trip
- assertSet compares loosely; assertSetStrict
- assertSee escapes HTML by default
basics
~10 sLivewire::test('newsletter-signup') renders the component in the test, set('email', ...) updates a property and call('subscribe') runs an action, each as a simulated Livewire request; then assertSet, assertSee and database assertions check the result.
solid answer
~40 s`Livewire::test()` takes a component name or class, plus an optional array passed to `mount()`, renders it through Livewire's own request cycle without a browser, and returns a chainable `Testable`. `set('email', '[email protected]')` sends a property update and `call('subscribe')` an action call, each as a real round trip of the signed snapshot, so hydration, update hooks and validation run as they would in production. `toggle('consent')` flips a boolean. Assert on state with `assertSet('subscribed', true)`, which compares loosely like `assertEquals`, or `assertSetStrict()` for `assertSame`; on output with `assertSee()`, which HTML-escapes the expected text; and on side effects with ordinary Laravel assertions such as `assertDatabaseHas()`. The same API works in Pest and PHPUnit.
code
php · 15 lines<?php
use App\Livewire\NewsletterSignup;
use Livewire\Livewire;
it('subscribes a new address', function () {
Livewire::test(NewsletterSignup::class)
->set('email', '[email protected]')
->toggle('consent')
->call('subscribe')
->assertSet('subscribed', true)
->assertSee('Check your inbox');
$this->assertDatabaseHas('subscribers', ['email' => '[email protected]']);
});go deeper
Remember the chain: Livewire::test(Component::class), then set(), call() and assertSet() or assertSee(), plus normal database assertions.
Explain that each set() and call() is a simulated request with hydration, hooks and validation, and know the loose versus strict and escaping defaults.
Write tests that pin behaviour rather than markup, choose strict assertions where types matter, and keep them fast enough to run on every change.
Decide how component tests, HTTP tests and browser tests share the testing budget for a Livewire-heavy app, and what each layer must prove.
## What `Livewire::test()` is A **Livewire component test** exercises one component in isolation, in PHP, with no browser. `Livewire::test()` accepts: - a component **name** (`'newsletter-signup'`, or a dotted name for nested folders) or a **class** (`NewsletterSignup::class`); - an optional **array of parameters** passed to `mount()`, the way a parent component or route would pass them. It performs the initial render and returns a `Livewire\Features\SupportTesting\Testable`, a fluent object whose methods all return `$this`, so a test reads as one chain. ## Driving the component | Method | What it simulates | |---|---| | `set('email', '[email protected]')` | A property update, as `wire:model` would send | | `set(['email' => '...', 'name' => '...'])` | Several updates, applied one by one | | `toggle('consent')` | `set('consent', ! current value)` | | `call('subscribe')` / `call('remove', $id)` | An action call with parameters | | `refresh()` | A request with no updates or calls | Each of these is a **full round trip**: the test takes the last snapshot, posts it to Livewire's update endpoint inside the test application with the update or call, and records the new HTML, snapshot and effects. That matters: hydration and dehydration really happen, so a property type Livewire cannot serialize fails here, `updated*` hooks and `#[Validate]` real-time validation run, and a `#[Locked]` property throws `CannotUpdateLockedPropertyException` when you `set()` it. ## Asserting - **State**: `assertSet('subscribed', true)` reads the property from the component instance. It compares with `assertEquals`, so `'1'` equals `1`; use `assertSetStrict()` when the type matters. A closure works too: `assertSet('tags', fn ($tags) => count($tags) === 2)`. `assertNotSet()` and `assertCount('tags', 2)` complete the set. - **Output**: `assertSee('Thanks for subscribing')` searches the rendered HTML, with Livewire's `wire:snapshot` data stripped, and **escapes** the expected string first, so `assertSee('Tom & Jerry')` looks for `Tom & Jerry`. Pass `false` as the second argument, or use `assertSeeHtml()`, to match raw markup. `assertDontSee()` is the negative. - **View data**: `assertViewHas('plans', ...)` checks what `render()` passed to the view. - **Side effects**: the component ran inside your test application, so ordinary Laravel assertions such as `assertDatabaseHas('subscribers', ...)` apply afterwards. ## A sign-up test ```php it('subscribes a new address', function () { Livewire::test(NewsletterSignup::class) ->set('email', '[email protected]') ->toggle('consent') ->call('subscribe') ->assertSet('subscribed', true) ->assertSee('Check your inbox'); $this->assertDatabaseHas('subscribers', ['email' => '[email protected]']); }); ``` ## Reading the component back Besides assertions, the `Testable` exposes the component for inspection: - `get('email')`, or simply `$component->email`, reads a property from the component **instance** after the last request, which is also what `assertSet()` uses. - `assertSnapshotSet('email', ...)` checks the **dehydrated** value instead, the JSON that would go to the browser. The two differ for synthesized types: an enum is an object on the instance but its backing value in the snapshot, and a model property's snapshot data is empty, because only its class and key travel, as metadata. - `instance()` returns the component object itself; `html()` returns the last rendered HTML; `assertReturned($value)` checks what the last action returned. - `dump()` and `dd()` print the last HTML when a test fails for reasons you cannot see. Prefer assertions on behaviour, such as properties, events and database rows, over long HTML fragments; markup changes more often than the rules it displays. ## Where the test file lives In Livewire 4, `php artisan make:livewire newsletter-signup --test` generates a Pest test next to a single-file component (`⚡newsletter-signup.test.php`); for a class-based component it goes under `tests/Feature/Livewire`. The Livewire docs recommend **Pest**, while the Laravel 13 skeleton ships **PHPUnit**; every `Testable` method works identically in both. Tests stored beside components need `resources/views` added to Pest's and PHPUnit's test paths. ## Common mistakes 1. Asserting on `$component->email` after a `set()` without realising `set()` already round-tripped; the property reflects the server's view, after hooks. 2. Using `assertSee()` with HTML in the expected string and wondering why escaped text never matches. 3. Expecting `assertSet()` to catch a type change from `int` to `string`; it is loose unless you use the strict variant. 4. Forgetting that `set()` works for properties no input binds, so a green test does not prove the form has a field for it.
- In a Livewire test, what is the difference between assertSet('count', '3') and assertSetStrict('count', '3') when the property holds the integer 3?`assertSet()` compares with `assertEquals`, so the string `'3'` and the integer `3` are equal and it passes. `assertSetStrict()` uses `assertSame`, which also compares types, so it fails. Use the strict form when a type change, for example from a cast or a typed property, is part of what you are testing.
- Why does a Livewire test fail when you set() a #[Locked] property, even though PHP code can change it?`set()` sends a real property update through Livewire's update cycle, exactly like the browser would. The locked attribute rejects client updates by throwing `CannotUpdateLockedPropertyException`, and in a test that exception reaches your test method. To put a locked property in a given state, pass it to `mount()` through the second argument of `Livewire::test()`.
saying these in an interview costs you the question
- Livewire::test() needs a browser or a running server.
- set() writes the property directly without running update hooks.
- assertSet() compares with strict type checking by default.
- assertSee() matches raw HTML unless told otherwise.
- Livewire component tests only work with Pest, not PHPUnit.