A Livewire::test() suite is green but the sign-up form breaks in the browser - what can a component test not prove, and what covers the gap?
answer
- middleware disabled for the simulated requests
- no JavaScript, no Alpine, no modifiers
- set() needs no bound input
- three exception types become responses
- assertSeeLivewire as the route smoke test
basics
~20 sLivewire::test() runs the component's PHP round trips with route middleware disabled and no browser, so it misses JavaScript, Alpine, wire:model modifiers, missing inputs, routing and middleware. An assertSeeLivewire HTTP test and a few browser tests cover those.
solid answer
~40 sA component test proves the PHP side: mounting, hydration of the real signed snapshot, update hooks, validation, actions, events and rendered HTML. It does not prove the page works. The simulated requests run `withoutMiddleware()`, so CSRF, `auth` and Livewire's persistent middleware never run; exception handling is off except for HTTP, authorization and model-not-found exceptions, which become responses you can assert. No JavaScript runs, so a typo in `wire:model`, a `.blur` or `.debounce` modifier, an Alpine `$wire` call or a `wire:ignore` widget is untested, and `set('email', ...)` passes even if no input binds `email`. Cover the gaps with an HTTP test that requests the route and calls `assertSeeLivewire('newsletter-signup')`, plus a small number of browser tests for critical flows.
go deeper
Know that Livewire::test() checks PHP logic only, and that a simple HTTP test with assertSeeLivewire checks the component is actually on the page.
Explain what the simulated requests switch off, middleware and most exception handling, and what they never run, JavaScript and template bindings.
Design a test split for a Livewire feature: many component tests, one HTTP smoke and auth test per page, and a few browser tests for JavaScript-heavy flows.
Own the testing strategy's cost and confidence trade-off across teams, including what gaps are accepted and how browser suites stay small and stable.
## What the component test actually runs `Livewire::test()` renders the component and then, for each `set()` or `call()`, posts the component's last **snapshot** with the update or call to Livewire's update endpoint, inside the test application. That is a faithful simulation of the **server half** of Livewire: - the snapshot is dehydrated and hydrated for real, checksum included; - `mount()`, `boot()`, `updated*()` and other hooks run in production order; - `#[Validate]` rules, `$this->validate()`, `#[Locked]`, `#[Authorize]` and action code all execute; - the Blade view renders, and dispatched events, redirects and `$this->js()` calls are recorded as effects. ## What it deliberately switches off Two settings make component tests fast and focused, and both hide real failures: 1. **Middleware is disabled.** The initial render happens on a temporary `/livewire-unit-test-endpoint/...` route the test registers, and every request runs with `withoutMiddleware()`. The `web` group's CSRF check, session start and `auth` on your page route never run, and Livewire's **persistent middleware** finds nothing to re-apply, because the recorded page is the temporary route. A page route that forgets `auth` still gets green component tests. 2. **Exception handling is mostly off.** Only `HttpException` (including `abort(403)`), `AuthorizationException` and `ModelNotFoundException` are rendered as responses, so `assertForbidden()` and `assertStatus(404)` work. Anything else, such as `CannotUpdateLockedPropertyException`, is thrown straight into the test. ## What it cannot see at all | Gap | Example that passes in `Livewire::test()` but fails for users | |---|---| | Template wiring | `wire:model="emial"` typo; `set('email', ...)` still works | | `wire:model` modifiers and timing | `.blur`, `.live`, `.debounce` behaviour | | Alpine and `$wire` code | An `x-on:click="$wire.subscribe()"` with a wrong method name | | JavaScript widgets | A `wire:ignore` date picker never writing to `$wire` | | Routing and layout | The component is not on the page, or the route 404s | | Browser-level behaviour | Focus, disabled buttons, `wire:loading`, navigation | `set()` is the subtle one: it sends an update for any public property, whether or not an input is bound to it. A green test shows the component handles an email; it does not show the page lets anyone type one. ## Exceptions in practice Because most exceptions escape the simulation, the shape of a component test changes with the failure you expect: ```php // Rendered to a response: assert on the status Livewire::actingAs($guest)->test(NewsletterAdmin::class) ->call('purgeList') ->assertForbidden(); // Thrown into the test: expect the exception $this->expectException(CannotUpdateLockedPropertyException::class); Livewire::test(NewsletterSignup::class)->set('listId', 99); ``` This is stricter than production, where the same locked-property write becomes a silent 419. It is a feature: a test cannot accidentally pass because an error page rendered. ## Covering the gaps 1. **An HTTP smoke test per page.** `$this->get('/newsletter')->assertOk()->assertSeeLivewire('newsletter-signup')` runs the real route with its middleware and proves the component renders on the page. `assertSeeLivewire` looks for the component's name in the rendered snapshot data and also accepts a class name. `assertDontSeeLivewire` is the inverse, useful for guests. 2. **An authorization test at the HTTP layer** for route middleware, since component tests skip it. 3. **A few browser tests** for flows where JavaScript matters: the sign-up with its real input, the double-opt-in confirmation, the date picker. Livewire 4 documents `Livewire::visit()` with Pest's browser plugin; Laravel also offers Dusk. Keep these few; they are slow. 4. **Many component tests** for the logic: validation branches, duplicate addresses, events, edge cases. ## A reasonable split for a sign-up component - Component tests: invalid email, duplicate email, consent missing, successful subscribe dispatches `subscribed`, rate-limit message. - HTTP test: `/newsletter` renders the component; the footer partial renders it on the home page. - One browser test: typing an email and pressing Subscribe shows the confirmation. The judgement an interviewer is probing is not "which tool is best" but whether you know what each layer **cannot** see, and put one cheap test at each boundary.
- Why does abort(403) inside a Livewire action show up as assertForbidden() in a component test, while CannotUpdateLockedPropertyException is thrown into the test?Livewire's test broker disables exception handling except for `HttpException`, `AuthorizationException` and `ModelNotFoundException`. Those three are rendered into responses, so status assertions work. Every other exception propagates to the test method, where you catch it with `expectException()` or let it fail the test.
- What exactly does assertSeeLivewire('newsletter-signup') look for in an HTTP test response?It JSON-encodes `['name' => 'newsletter-signup']`, HTML-escapes it, and checks the response body contains that fragment, which appears in the component's `wire:snapshot` data. A class name is resolved to the component name first. It proves the component rendered on the page, not that it works.
Bench-testing a car engine: it proves the engine runs, fuels and fires correctly, but not that the pedals are connected to it.
saying these in an interview costs you the question
- Livewire::test() runs the page route's auth and CSRF middleware.
- A passing set('email', ...) proves the form has an email input.
- Component tests exercise wire:model.blur and debounce timing.
- Every exception in a component test becomes an HTTP status to assert.
- Browser tests should replace component tests for Livewire apps.