natcave's avatar
Level 10

Prevent lazy loading in tests.

Hey there. I'm encountering a perplexing issue and hope I'm just missing something obvious.

In my AppServiceProvider, I have the following line set:

Model::preventLazyLoading(! app()->isProduction());

My setup involves three environments: production, development, and local. While everything functions as expected in production, I often run into a LazyLoadingViolationException in my development environment. This is puzzling because I would expect any violations to also surface in my testing environment, assuming testing is handled similarly. I'm not sure if my assumption is incorrect or if there's something amiss in my approach.

To illustrate, consider this example where staff_member is related to business. In a test, I have:

$staff_member = StaffMember::factory()->create();
dd(
    'preventsLazyLoading: '. ($staff_member->preventsLazyLoading() ? 'true' : 'false'),
    'relationship_loaded: '. ($staff_member->relationLoaded('business') ? 'true' : 'false'),
    'business_name: '. $staff_member->business->name,
);

The output I receive is:

"preventsLazyLoading: true"
"relationship_loaded: false"
"business_name: Crooks Ltd"

This is contrary to what I would expect. Ideally, I'd want the LazyLoadingViolationException to be thrown during testing, alerting me to places where I need to implement eager loading. This would prevent encountering these violations unexpectedly in my development environment and prevent them from staying hidden in production. Thankfully, production is functioning correctly, but catching these violations earlier (before they reach my development server) would be greatly beneficial.

I did a brief review of the framework source code and found no exceptions specifically for testing environments. I appreciate any insight you may have. Thanks in advance.

0 likes
6 replies
LaryAI's avatar
Level 58

It sounds like you want to ensure that lazy loading is prevented in your tests, just as it is in your development environment, so that you can catch any LazyLoadingViolationException during the testing phase. Here's what you can do to ensure that lazy loading is prevented in your tests:

  1. Make sure that your testing environment is not considered as "production" by your application. You can check this by looking at the APP_ENV value in your .env.testing file. It should be set to something other than production, typically testing.

  2. In your phpunit.xml file, ensure that the environment variable APP_ENV is set to testing (or whatever non-production environment you use for testing).

  3. In your base test class (usually tests/TestCase.php), you can explicitly set the prevention of lazy loading in the setUp method. This ensures that it's always active for every test.

Here's an example of how you might set this up in your TestCase.php:

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Database\Eloquent\Model;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected function setUp(): void
    {
        parent::setUp();

        // Prevent lazy loading in all tests
        Model::preventLazyLoading(true);
    }
}

By doing this, you're ensuring that lazy loading is prevented in all of your tests, regardless of the environment configuration. This should help you catch any LazyLoadingViolationException during the testing phase, before they can cause issues in your development or production environments.

Remember to import the Model facade at the top of your TestCase.php file if it's not already there:

use Illuminate\Database\Eloquent\Model;

If you follow these steps and still encounter issues, make sure that your test environment is correctly configured and that no other part of your test setup is overriding the preventLazyLoading setting.

natcave's avatar
Level 10

Thanks. For further clarification:

  • I'm not using a .env.testing file
  • APP_ENV is set to testing in my phpunit.xml
  • adding Model::preventLazyLoading(true); to TestCase did not prevent lazy loading in my tests
natcave's avatar
Level 10

Hey @s4muel, thanks so much for your reply.

Your explanation makes sense to me. To help illustrate, I've constructed a better example. I tried the following code in a test, and again it did not produce the expected LazyLoadingViolationException:

$business = Business::factory()
            ->has(StaffMember::factory()->count(2), 'staff_members')
            ->create();

dd(
    'preventsLazyLoading: '. ($business->preventsLazyLoading() ? 'true' : 'false'),
    'relationship_loaded: '. ($business->relationLoaded('staff_members') ? 'true' : 'false'),
    'business_name: '. $business->staff_members->first()->name,
);

The output I receive is:

"preventsLazyLoading: true"
"relationship_loaded: false"
"business_name: hansen.rowland"

It certainly feels like I'm missing something, or perhaps lazy loading is simply not checked in tests at all. I seem to run into this issue at least once a day, so it would be wonderful to find a solution.

If you have any further insight, please share. Or, if I can provide more info, please let me know. Thanks!

natcave's avatar
Level 10

I also tried:

$businesses = Business::factory()
            ->count(2)
            ->has(StaffMember::factory()->count(2), 'staff_members')
            ->create();

dd(
    'business_name: '. $businesses->first()->staff_members->first()->name,
);

The result is:

"business_name: block.scottie"

No LazyLoadingViolationException

s4muel's avatar

@natcave in this case you have two businesses, but you call the staff_members relation on a single one of them, so it is not triggered. try it like this:

$businesses = Business::factory()
            ->count(2)
            ->has(StaffMember::factory()->count(2), 'staff_members')
            ->create();

foreach($businesses as $business) {
    //this should raise the exception, because you call the relationship for more models one at a time, thus creating a new query for each loop (since the relation is not eager loaded)
    dump($business->staff_members->first()->name);
}

Please or to participate in this conversation.