livewire/livewire: [4.x] Allow missing model status assertions in component tests
Contributed by calebporzio
Part of the livewire v4.4.7 release
Pull request #10720 closes an annoying gap in Livewire's testing story: component tests could not assert a 404 when a Livewire action hit Eloquent's findOrFail() (or firstOrFail()) on a missing model. The test request broker now allows Illuminate\Database\Eloquent\ModelNotFoundException through to Laravel's exception handler, which converts it into the same 404 response your application produces in production. For anyone upgrading from v3.8.10 into 4.x, this is a low-risk, test-only fix: no component, routing, or application error-handling changes — just assertNotFound() and assertStatus(404) finally working the way you would expect.
Why the test failed while the app returned a 404
In a normal request cycle, a missing model is straightforward: findOrFail() throws ModelNotFoundException, and Laravel's exception handler renders it as a 404. Livewire's test harness has to be more careful. When you call Livewire::test(), the broker in RequestBroker.php temporarily disables exception handling and middleware so assertions can inspect the component's request. Crucially, it keeps a small allowlist of exceptions that should still reach the application's handler — previously HttpException and AuthorizationException — precisely so status assertions like assertForbidden() and assertNotFound() keep working.
ModelNotFoundException was not on that list. Instead of becoming a 404, it escaped the test request and surfaced as a raw error before the assertion could ever run:
// Failed on v3.8.10 with ModelNotFoundException,
// instead of asserting the 404 the app would produce
Livewire::test(Users::class)
->call('showFullDetails', 0)
->assertNotFound();
The behavior was reported in discussion #9447 and reproduced on current 4.x using an actual Eloquent collection's findOrFail(), confirming it was not an artifact of a mocked or unusual lookup.
The fix: one more exception on the allowlist
The production code change is deliberately tiny — a single import plus one class added to the existing allowlist in src/Features/SupportTesting/RequestBroker.php:
$this->withoutExceptionHandling([
HttpException::class,
AuthorizationException::class,
ModelNotFoundException::class, // new
])->withoutMiddleware();
Anything on this list is handed to the application's exception handler during the test request, so it is converted into the HTTP response your app would really send — ModelNotFoundException becomes a 404, exactly as authorization exceptions already become 403s. Nothing about how your application handles requests changes; this only affects Livewire's internal test request broker.
What your tests can do now
Given a component that looks a model up by ID:
class Users extends Component
{
public function showFullDetails($id)
{
$this->user = User::findOrFail($id);
}
}
A missing record now asserts like any other HTTP status:
Livewire::test(Users::class)
->call('showFullDetails', 999) // no such user
->assertNotFound(); // now passes
If you specifically want to pin down that the action itself is what throws, opt out explicitly and the old behavior returns:
$this->withoutExceptionHandling();
$this->expectException(ModelNotFoundException::class);
Livewire::test(Users::class)->call('showFullDetails', 999);
Upgrade impact
There is nothing to migrate. The fix only touches Livewire's testing internals, so existing components, actions, and application exception handling behave exactly as before. Two practical notes:
- Tests that previously errored with an uncaught
ModelNotFoundExceptionwill now receive a 404 response. That is almost certainly what you wanted, but if a test quietly depended on the exception bubbling out by default, add an explicitwithoutExceptionHandling()call to keep it. - Livewire's status assertions now match the framework's controller-test conventions more closely:
assertNotFound(),assertForbidden(),assertUnauthorized(), and friends all work against the responses Laravel's handler generates, whether the exception originated in your action or the framework.
How it was validated
The PR adds a MissingModelComponent fixture whose action calls (new Collection)->findOrFail($id), plus two regression tests: one asserting the 404 path works end to end, and one proving that withoutExceptionHandling() still rethrows the exception. Coverage also includes the explicit opt-out, the existing 401/403/404 assertions, unexpected action errors still propagating, and restoration of the application's exception handler and middleware state after each test. The 404 regression failed before the change and passes after; on PHP 8.4.6 with Laravel 13.31.0, the relevant suites report 64 passing tests and 237 assertions:
vendor/bin/phpunit src/Features/SupportTesting/Tests/TestableLivewireCanAssertStatusCodesUnitTest.php src/Features/SupportTesting/UnitTest.php
Takeaways
ModelNotFoundExceptionthrown inside a Livewire action now becomes a 404 in component tests, soassertNotFound()andassertStatus(404)work as expected.- The change is confined to the exception allowlist in
RequestBroker.php; application request handling is untouched. - Explicit
withoutExceptionHandling()still surfaces the raw exception if you want to assert it directly. - No upgrade steps are required coming from v3.8.10 — tests that used to error on missing models will now assert cleanly.