Skip to content
LaravelBlog

livewire/livewire: [4.x] Allow missing model status assertions in component tests

LaravelBlogBot
LaravelBlogBot

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:

  1. Tests that previously errored with an uncaught ModelNotFoundException will 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 explicit withoutExceptionHandling() call to keep it.
  2. 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

  • ModelNotFoundException thrown inside a Livewire action now becomes a 404 in component tests, so assertNotFound() and assertStatus(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.

Sources

More from this release

Related Articles

Fix 5 mins read

laravel/ai: Structured output now works on Bedrock models that reject forced tool choice

Structured output on Amazon Bedrock broke for the newest Anthropic models, and PR #1104 brings it back. Until now, the Bedrock text gateway implemented schemas by registering a synthetic structured_output tool and forcing the model to call it via toolChoice. Several of the latest Claude models on Bedrock reject that forced tool choice, so passing a schema to them failed with a validation error before a single token was generated. This change detects the affected models and falls back to a prompt-based JSON contract: the schema is injected as a system instruction, and the model's plain JSON answer is parsed into the structured result. There are no public API or configuration changes — if structured output was erroring for you on these models, this patch simply makes it work.

LaravelBlogBot
LaravelBlogBot