skip to content

Why can calling withoutMiddleware() make a Laravel checkout test pass while the real endpoint fails, and what should you do instead?

level: seniorimportance: should knowfreq 26%

answer

  1. binds middleware.disable to true
  2. global and route middleware both skipped
  3. SubstituteBindings skipped: empty model injected
  4. auth, can and throttle vanish too
  5. withoutMiddleware(Class) swaps just one

basics

~20 s

withoutMiddleware() with no arguments skips every global and route middleware, so authentication, authorization, throttling, sessions and route model binding all stop running. The test then checks a different endpoint than production; disable one class with withoutMiddleware(Foo::class), or satisfy the middleware.

solid answer

~40 s

With no argument, `withoutMiddleware()` binds `middleware.disable` to `true`; the HTTP kernel then skips the global stack and the router skips route middleware. That removes `auth`, `can:` checks, `throttle`, session start and `SubstituteBindings`. Losing the last one is the nastiest: a controller that type-hints `Order $order` no longer gets the bound model, and the router resolves the class from the container instead, handing it a new, empty `Order`. So a test can pass against an endpoint that production protects or binds differently. Prefer satisfying the middleware, for example with `actingAs()`, or disable one class: `withoutMiddleware(EnsureCartIsNotEmpty::class)` binds that class to a pass-through. CSRF needs no disabling in tests, because it is already skipped.

code

php · 27 lines
php
<?php

namespace Tests\Feature;

use App\Http\Middleware\EnsureCartIsNotEmpty;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class OrderPageTest extends TestCase
{
    use RefreshDatabase;

    public function test_owner_sees_the_order(): void
    {
        $customer = User::factory()->create();
        $order = Order::factory()->for($customer)->create();

        // auth, can: and binding all still run
        $this->actingAs($customer)
            ->withoutMiddleware(EnsureCartIsNotEmpty::class)
            ->get("/orders/{$order->id}")
            ->assertOk()
            ->assertViewHas('order', $order);
    }
}

go deeper

for a junior

Know that withoutMiddleware() exists and that it turns off far more than CSRF, so tests using it may not reflect the real endpoint.

for a middle

Explain the two modes: all middleware via middleware.disable, or one class swapped for a pass-through, and list what auth and binding loss changes.

for a senior

Recognise the empty-model trap from skipped SubstituteBindings, prefer actingAs and precise class removal, and flag the trait on access-control tests in review.

for a principal

Set a review rule that tests asserting access or binding never disable all middleware, so the suite's green status keeps meaning what it claims.

## What the helper does `withoutMiddleware()` is a method on Laravel's base test case, with two modes: - **No argument.** It binds `middleware.disable` to `true` in the container. The HTTP kernel checks `shouldSkipMiddleware()` and sends the request through **no global middleware**, and the router checks the same binding and gathers **no route middleware**. - **A class or list of classes.** It binds each class name to an anonymous object whose `handle()` just calls `$next($request)`. When the pipeline resolves that middleware from the container, it gets the pass-through instead. A `WithoutMiddleware` trait applies the no-argument form to every test in a class. ## What disappears with everything For a sneaker shop's `GET /orders/{order}` and `POST /checkout`, turning off all middleware removes: | Middleware | What the test stops checking | |---|---| | `auth` | guests are turned away | | `can:view,order` | one customer cannot see another's order | | `throttle:checkout` | rate limits apply | | session start and error sharing | flashed errors and old input exist | | cookie encryption | cookies are decrypted as in production | | `SubstituteBindings` | `{order}` becomes an `Order` model | The last row produces the most confusing failures, or worse, the most confusing passes. ## The empty-model trap Route model binding runs inside the `SubstituteBindings` middleware. With it skipped, the route parameter stays the raw string `"42"`. When the router then calls `show(Order $order)`, its dependency resolver sees a class-typed parameter with no matching instance among the parameters and **resolves `Order` from the container**, which builds a new, unsaved, empty model. The controller runs happily against an order with no id, no items and no owner: 1. an authorization check comparing `$order->user_id` with the user may compare `null` and behave oddly; 2. a view may render an empty order page with 200 OK; 3. a test asserting `assertOk()` passes, while in production the same URL returns the real order, or 404 for a missing one. The test was green for the wrong reason, and it would keep passing even if the real binding broke. ## What to do instead In order of preference: 1. **Satisfy the middleware.** Use `actingAs($customer)` for `auth`, create data that passes `can:` checks, and let bindings run. The test then covers the endpoint as deployed. 2. **Disable one class, precisely.** `withoutMiddleware(EnsureCartIsNotEmpty::class)` removes only that check for a test that is about something else, and leaves binding and auth in place. 3. **Fake what the middleware talks to.** If a middleware calls an external service, replace that service in the container rather than removing the middleware. 4. **Disable everything only for a narrow reason,** such as a test of a helper that happens to need a request, and never on tests that assert access rules. Two things are not reasons to reach for it. CSRF protection is already skipped when the app runs unit tests, so a `post()` needs no token. And the `#[WithoutMiddleware]` controller attribute or a route's `withoutMiddleware()` method are routing features that change production behaviour; they are not test tools. ## When removing everything is acceptable There are narrow cases where the no-argument form is reasonable: - a test of a response macro or a view composer that happens to need a request, where no route logic matters; - a characterization test of a legacy endpoint while its middleware is being rewritten, deleted once the rewrite lands; - an experiment timing a controller in isolation. In each case the test should say so in its name, and it should not assert on status codes that middleware decides, such as 401, 403 or 429. A useful team rule is that any no-argument `withoutMiddleware()` call needs a comment naming the middleware it meant to avoid; answering that usually leads to passing a class name instead. ## How to spot it in review - A test class using the `WithoutMiddleware` trait while also asserting 403s or redirects to login. - Controller tests that pass for URLs with ids that do not exist. - `assertOk()` on routes whose middleware group includes `auth`, with no `actingAs()` in sight. Interviewers ask this as a senior question because the fix is judgment, not syntax: knowing that a green test proves only what actually ran.

  • With withoutMiddleware(), why does GET /orders/999 return 200 instead of 404 in a Laravel test?
    The 404 for a missing model comes from route model binding in `SubstituteBindings`, which was skipped. The parameter stays the string `"999"`, and the router resolves the type-hinted `Order` from the container, building an empty model. The controller renders it, so the response is 200.
  • When does withoutMiddleware(SomeMiddleware::class) fail to remove a middleware?
    It works by binding that class name in the container to a pass-through object, so it only affects middleware the pipeline resolves by that exact class name. Pass the class the route actually resolves, not a different class or an unrelated alias, or the real middleware still runs.

saying these in an interview costs you the question

  • withoutMiddleware() only turns off CSRF protection for the test.
  • Route model binding happens in the router whether or not middleware runs.
  • withoutMiddleware() leaves global middleware running and skips only route middleware.
  • Tests need withoutMiddleware() to post forms without a CSRF token.
  • The WithoutMiddleware test trait is a safe default for every feature test class.