I Enabled Strict Mode In Laravel 12 and My Tests Started Failing Everywhere


So there I was, being a good developer. Writing tests, enabling strict mode, doing all the right things. Then I run my test suite and suddenly everything is red.

MissingAttributeException: The attribute [seller_approval_status] either does not exist or was not retrieved for model [App\Models\User].
Enter fullscreen mode

Exit fullscreen mode

But here’s the thing – my tests were passing before. The code hasn’t changed. And when I manually test the feature in my browser, it works perfectly fine.

What the hell is going on?

Turns out, Laravel 12’s strict mode has this fun quirk where it makes your tests explode in ways that have nothing to do with whether your actual code works. Let me explain the nightmare I just lived through so you don’t have to.

The Feature That Sounded Amazing

Laravel 12’s Model::shouldBeStrict() looked like a gift from the coding gods, Don’t get me wrong, this will save you time. One line of code that protects you from common mistakes:

// In AppServiceProvider::boot()
Model::shouldBeStrict();
Enter fullscreen mode

Exit fullscreen mode

This single line activates three safety nets:

  1. preventSilentlyDiscardingAttributes() – Yells at you when you try to mass-assign attributes that aren’t fillable.

  2. preventLazyLoading() – Catches those sneaky N+1 queries before they murder your database.

  3. preventAccessingMissingAttributes() – Stops you from accessing attributes that don’t exist.

The first two? Absolutely brilliant. Game changers. The third one? Yeah, that’s where things went sideways.

Here’s Where Everything Falls Apart

Here’s something I didn’t know: Laravel’s authentication doesn’t load all your user data. I mean, it makes sense when you think about it. Why fetch 20 columns when you only need 5 for authentication? So when you call $this->actingAs($user), Laravel does something like this under the hood:

// What Laravel actually does internally
$user = User::select([
    'id', 
    'email', 
    'password', 
    'remember_token', 
    'email_verified_at'
])->find($user->id);
Enter fullscreen mode

Exit fullscreen mode

Notice what’s missing? Your custom columns. All those nice fields you added for your business logic? Not there. And that’s when strict mode loses its mind.

The Test That Should Have Worked

Here’s the test I was writing nothing fancy, just checking if a user can access their dashboard:

test('seller with pending approval sees pending page', function () {
    $user = User::factory()->create([
        'seller_approval_status' => 'pending'
    ]);

    $response = $this->actingAs($user)->get('/dashboard');

    $response->assertOk();
    $response->assertViewIs('pending');
});
Enter fullscreen mode

Exit fullscreen mode

Here’s what happened:

  • The seller_approval_status column exists in your database. It’s right there.
  • But Laravel didn’t SELECT it when loading the authenticated user (to save memory)
  • Strict mode sees you trying to access an attribute that’s not loaded
  • Strict mode panics and throws an exception

It’s technically protecting you from a problem that doesn’t exist. Classic overzealous bouncer situation. The Real Problem: actingAs() is Broken (Sort Of)

The kicker? The browser version works fine. When real users log in, Laravel loads the user differently. So your tests fail while your actual app works. Or sometimes it’s the other way around. Absolutely maddening.



It Gets Worse in Weird Ways

The bugs in Laravel 12’s strict mode make testing even more unpredictable:

1. Factory data sometimes works
If you access the attribute immediately after creating the model, it works:

$user = User::factory()->create(['role' => 'admin']);
echo $user->role; // ✅ Works fine
Enter fullscreen mode

Exit fullscreen mode

But after actingAs():

$this->actingAs($user);
$user = auth()->user();
echo $user->role; // 💥 Exception
Enter fullscreen mode

Exit fullscreen mode

2. The :memory: database lie
Using SQLite’s :memory: database? Your tests might pass locally and fail in CI, or vice versa. The in-memory database handles default values differently than file-based databases.

3. Some attributes work, others don’t
Casted attributes sometimes bypass the check. Sometimes. I never figured out the pattern, and I don’t think Laravel has either.

I wasted an entire afternoon trying to figure out why email_verified_at worked but seller_approval_status didn’t. So the problem is that the attribute literally doesn’t exist on the model instance when actingAs() reloads it, which is exactly why we get the exception.

So how do we fix this?

There are a couple of ways to go about this, but this is what I did instead. After trying a bunch of different approaches, here’s what actually worked for my test suite.

Add default values to your User model for any attribute you access in tests:

class User extends Authenticatable
{
    protected $fillable = [
        'seller_approval_status',
    ];

    // This one line fixed all my tests
    protected $attributes = [
        'seller_approval_status' => null,
    ];
}
Enter fullscreen mode

Exit fullscreen mode

Now, when actingAs() loads a partial user, these attributes exist with default values. If they were loaded from the database, the real values are used. If not, the defaults kick in.

My tests went from red to green instantly.

Why this works for testing:

  • Your factory still sets the real values
  • When the test creates the user, it has the factory data
  • When actingAs() reloads the user partially, defaults fill in the gaps
  • Your controller code accesses the attributes without exceptions
  • Zero performance hit

The only downside:

  • You need to identify which attributes are causing test failures (run your tests, they’ll tell you)
  • Have to pick reasonable defaults (usually null is fine)



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *