Kovah's avatar
Level 5

Eager loading of a model relation works in browser, but not in PHPunit test

I have a really weird issue that I am not able to solve right now. The full code is available on Github.

Status Quo

Currently, I have the standard user model and a settings model. A setting is bound to a user via a user_id field. The user model has a $user->rawSettings relation which eager-loads the related settings. I use eager loading here to prevent database calls for each single setting checked in the application. All settings are transformed into a $key => $value collection and available via a $user->settings() method. There is a helper function usersettings($key) which returns a setting for the current user like so: auth()->user()->setting($key).

This is working without issues in the application while running in the browser.

The issue

However, I implemented tests for some features and the helper function stopped working (corresponding test). I thought that I may have missed something so I double checked the process:

  • auth()->user() returns the current user with the ID 1
  • Setting::first() returns a valid setting entry connected to the user via user_id = 1
  • auth()->user()->rawSettings is empty, despite the fact that there are entries in the database?!
  • auth()->user()->rawSettings()->get() correctly returns all entries. WTF?!
  • usersettings('...') therefore returns null for every key.

Why is that eager loaded relation for user settings working in the browser, but not in the PHPunit test? Why are eager-loaded relations empty while the non-eager-loaded relations correctly return all entries?

Tried solutions

I already tried adding the load('rawSettings') method to the helper function (auth()->user()->load('rawSettings')->...), which then returns the correct results. But this leads to numerous queries being executed for each single call uf the usersettings() method. This is not a proper solution for me.

I tried adding the $with = ['rawSettings'] property to the user model as suggested here. Nothing changes, the relations are still not loaded in the unit tests.

0 likes
6 replies
ahmeddabak's avatar

On your User model can you post the code of the rawSettings method

Kovah's avatar
Level 5

rawSettings is a simple HasMany relation.

From the public repo on Github:

public function rawSettings(): HasMany
{
    return $this->hasMany(Setting::class, 'user_id', 'id');
}

public function settings()
{
    return $this->rawSettings->mapWithKeys(function ($item) {
        return [$item['key'] => $item['value']];
    });
}
bugsysha's avatar

Like @ahmeddabak said. You are confusing that something is not working with the lifecycle of a request. Reason why it is working is that framework gets reloaded when you are redirected back after creating user settings. While in tests you are within single request lifecycle.

My advice is to stay within that boundary of one request lifecycle and test only stuff available. Setup everything in the database and then just test your usersettings function if you want to test that function and not after the post request for creation of settings.

Kovah's avatar
Level 5

Many thanks for your answers. I managed to fix the issue by checking if the settings are empty and then reload them once. Seems to work correctly.

As far as I understand, the issue was the request cycle. I know that PHPunit runs each test as a single request, therefore the authenticated user was "logged in" on each requests. I assume that while this authentication process happens and the auth guard retrieves the user data, relations are checked. What I missed is the fact, that the settings are created after this proces, leading to an empty relation for the user, while the settings are already there (pulled from the database) when the users accesses the app via the browser on subsequent requests. Is that correct?

bugsysha's avatar

I think that relations should be retrieved with user when they are defined in $with property. There is no reason to do it differently when the user is retrieved by AuthManager.

Please or to participate in this conversation.