Laravel testing Authentication. Failed on subsequent login attemtps with Illuminate\Database\Eloquent\ModelNotFoundException

Viewed 701

I'm trying to test Laravel with different users roles.

I have made 4 tests (actually, 2 tests with 2 different approach):

use RefreshDatabase;

/** @test */
public function if_administrators_can_view_user_list_post()
{
    $this->seed();

    $user = User::find(1);
    $credentials = [
        'email'     => $user->email,
        'password'  => 'password'
    ];

    $response = $this->post(route('login'), $credentials);

    $this->get(route('admin.users.index'))
        ->assertStatus(200);

    $this->post(route('logout'));
}

/** @test */
public function if_users_cannot_view_user_list_post()
{
    $this->seed();

    $user = User::find(2);
    $credentials = [
        'email'     => $user->email,
        'password'  => 'password'
    ];

    $response = $this->post(route('login'), $credentials);

    $this->get(route('admin.users.index'))
        ->assertStatus(403);

    $this->post(route('logout'));
}

/** @test */
public function if_administrators_can_view_user_list_auth()
{
    $this->seed();

    $user = User::find(1);

    if (Auth::attempt(['email' => $user->email, 'password' => 'password'])) {
        $this->get(route('admin.users.index'))
            ->assertStatus(200);

        Auth::logout();
    } else {
        $this->assertTrue(false);
    }
}

/** @test */
public function if_users_cannot_view_user_list_auth()
{
    $this->seed();

    $user = User::find(2);

    if (Auth::attempt(['email' => $user->email, 'password' => 'password'])) {
        $this->get(route('admin.users.index'))
            ->assertStatus(403);

        Auth::logout();
    } else {
        $this->assertTrue(false);
    }
}

The database is seeded with 2 users. User id = 1 is an administrator, and user id = 2 is an ordinary user.

If I run each test separately, all tests are OK:

PS C:\laravel> php artisan test --filter if_administrators_can_view_user_list_post
Warning: TTY mode is not supported on Windows platform.

   PASS  Tests\Feature\Routes\UserRoutesTest
  ✓ if administrators can view user list post

  Tests:  1 passed
  Time:   1.51s

PS C:\laravel> php artisan test --filter if_users_cannot_view_user_list_post
Warning: TTY mode is not supported on Windows platform.

   PASS  Tests\Feature\Routes\UserRoutesTest
  ✓ if users cannot view user list post

  Tests:  1 passed
  Time:   1.54s

PS C:\laravel> php artisan test --filter if_administrators_can_view_user_list_auth
Warning: TTY mode is not supported on Windows platform.

   PASS  Tests\Feature\Routes\UserRoutesTest
  ✓ if administrators can view user list auth

  Tests:  1 passed
  Time:   1.45s

PS C:\laravel> php artisan test --filter if_users_cannot_view_user_list_auth
Warning: TTY mode is not supported on Windows platform.

   PASS  Tests\Feature\Routes\UserRoutesTest
  ✓ if users cannot view user list auth

  Tests:  1 passed
  Time:   1.50s

But, if I run all tests, the second and others failed with Illuminate\Database\Eloquent\ModelNotFoundException exception:

PS C:\laravel> php artisan test 

   FAIL  Tests\Feature\Routes\UserRoutesTest
  ✓ if administrators can view user list post
  ⨯ if users cannot view user list post
  ⨯ if administrators can view user list auth
  ⨯ if users cannot view user list auth

• Tests\Feature\Routes\UserRoutesTest > if users cannot view user list post
   Illuminate\Database\Eloquent\ModelNotFoundException 

  No query results for model [App\Models\User] 1

  at C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:434
    430▕         } elseif (! is_null($result)) {
    431▕             return $result;
    432▕         }
    433▕
  ➜ 434▕         throw (new ModelNotFoundException)->setModel(
    435▕             get_class($this->model), $id
    436▕         );
    437▕     }
    438▕

  1   C:\laravel\vendor\laravel\framework\src\Illuminate\Support\Traits\ForwardsCalls.php:23
      Illuminate\Database\Eloquent\Builder::findOrFail()

  2   C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php:1993
      Illuminate\Database\Eloquent\Model::forwardCallTo(Object(Illuminate\Database\Eloquent\Builder), "findOrFail")

  • Tests\Feature\Routes\UserRoutesTest > if administrators can view user list auth
   Illuminate\Database\Eloquent\ModelNotFoundException 

  No query results for model [App\Models\User] 1

  at C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:434
    430▕         } elseif (! is_null($result)) {
    431▕             return $result;
    432▕         }
    433▕
  ➜ 434▕         throw (new ModelNotFoundException)->setModel(
    435▕             get_class($this->model), $id
    436▕         );
    437▕     }
    438▕

  1   C:\laravel\vendor\laravel\framework\src\Illuminate\Support\Traits\ForwardsCalls.php:23
      Illuminate\Database\Eloquent\Builder::findOrFail()

  2   C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php:1993
      Illuminate\Database\Eloquent\Model::forwardCallTo(Object(Illuminate\Database\Eloquent\Builder), "findOrFail")

  • Tests\Feature\Routes\UserRoutesTest > if users cannot view user list auth
   Illuminate\Database\Eloquent\ModelNotFoundException 

  No query results for model [App\Models\User] 1

  at C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:434
    430▕         } elseif (! is_null($result)) {
    431▕             return $result;
    432▕         }
    433▕
  ➜ 434▕         throw (new ModelNotFoundException)->setModel(
    435▕             get_class($this->model), $id
    436▕         );
    437▕     }
    438▕

  1   C:\laravel\vendor\laravel\framework\src\Illuminate\Support\Traits\ForwardsCalls.php:23
      Illuminate\Database\Eloquent\Builder::findOrFail()

  2   C:\laravel\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php:1993
      Illuminate\Database\Eloquent\Model::forwardCallTo(Object(Illuminate\Database\Eloquent\Builder), "findOrFail")


  Tests:  3 failed, 1 passed
  Time:   1.84s

Does anyone has any clue to this?

1 Answers

Okay, let's tackle some small details first:

  1. Never write a test that asserts 2 different (or more) behaviors in the same test. A test should only test 1 thing. if_administrators_can_view_user_list_auth, should be user_list_vieable_by_administrator or user_list_vieable_when_user_is_logged_in_as_administrator or anything similar. You have to create the inverse too, user_list_not_vieable_when_user_is_not_admin and in this one you can test all possible user's types that are not administrator type.
  2. Never write a test name as a conditional (at least using if). For example, if you want to test if a user can post a message in an X app, write the test as test_user_can_create_post_when_logged_in instead of if_user_is_logged_in_create_post or anything similar. This is sort of a convention. Read about User Stories and Acceptance Criteria, there is a way of writing this, you should follow Acceptance Criteria writing convention.
  3. Instead of blindly seeding, my personal recommendation is to ONLY HAVE IN YOUR DATABASE what you are going to test, it doesn't matter if you create entries for a model you will never use, maybe that could alter something in your system and you are not taking that into account.
    • Let's say the user does not need to have an Avatar uploaded when they Post something, but your code is wrong and you REQUIRE an avatar. Because you are bindly seeding anything (even stuff that you should not require for your test), your test could pass, but behind scenes, you required it for it to work, do you follow me ?
  4. Remember you can use dataProviders on PHPUnit to feed different data to your test, like point number 2 I mentioned here. Instead of creating a lot of different types of users, you can feed them and run a single test on each of the types, so when any fails, you can see which one failed, and also your test does not get bloated with setup data.
  5. Never use route helper when testing. For example, if you desired route is POST /user/post and the name is user.post.store, you will do $this->post(route('user.post.store'), [...]);, but what if user.post.store URL is like /asd/123/bad/url-5555 ? You are not testing that you access a desired URL and does what you want. So you must test everything, even if you have to type it like a robot, you MUST test everything and not rely on anything like that, because it is a feature test, so you are testing EVERYTHING you can. If you could change frameworks and the code is still the same, it should still be working (let's say you don't use route helper), it should still be accessible by your desired URL, etc. Don't use helpers except for asserting stuff.

So, to help you a little more, you should not bindly seed everything. Remember you can use $this->seed(SeederName::class);, if you use ->seed(); it will use default seeder. You can even pass an array with more than 1 seeder.


I am now going to rewrite your tests so you can see what can be done better:

/** @test */
public function user_list_vieable_when_user_is_logged_in_as_administrator()
{
    /** @var User $user Remember this user is an admin, set up that */
    $user = User::factory()->create();

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

    $response->assertOk();
}

That test is exactly the same as if_administrators_can_view_user_list_post, but see that:

  • I did not blindly seed anything, just what I needed. I may need to factory something more to make that user an admin but this is an example, you know what to set up to make that happen.
  • I did not literally login, but I "simulated" login in. If you want to simulate being logged in, you have to use $this->actingAs($model, 'guard');, you can even specify which guard to use as the second parameter.
  • You don't need to log out, as the next test will be "reseted" so you will have no session. Fresh like a new test.
  • You can also use some "synonyms", for example assertOk() is exactly the same as ->assertStatus(200), you can use ->assertSuccessful() and that would be status >= 200 && status < 300. But this is really personal. See all the available asserts as there are more about this.

Now let's continue with the other ones:

/** @test */
public function user_list_forbidden_when_user_is_not_admin()
{
    /** @var User $user Remember this user is not an admin, set up that */
    $user = User::factory()->create();

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

    $response->assertForbidden();
}

Again, I just created the user with needed permissions, and tested against the desired route, then asserted it was forbidden. I would personally return 401 (unauthorized) as 403 is more special, but it is fairly similar.

In this case, you could use dataProvider as I mentioned before and test each type of user, so you are 100% sure that it will be forbidden for any user that is not an admin or any type you want.

Let's quickly "simulate" it as I have no idea about your schema. Let's say that we have a role column in your User table, so you could do this:

/**
 * @test
 * @dataProvider
 */
public function user_list_forbidden_when_user_is_not_admin(string $type)
{
    /** @var User $user */
    $user = User::factory()->$type()->create();

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

    $response->assertForbidden();
}

public function common_user_types_data_provider()
{
    return [
        'Normal' => ['normal'],
        'Moderator' => ['moderator'],
        'Editor' => ['editor'],
        'Vip' => ['vip']
    ];
}

I have taken advantage of states, but you can send anything you need, except data or classes that must be run AFTER the framework is ready. dataProviders run BEFORE the framework is ready, so you cannot use anything about it.

Following your tests order, I am not 100% sure what you wanted to test in if_administrators_can_view_user_list_auth and if_users_cannot_view_user_list_auth, but I am assuming you wanted to test something similar to the previous tests, but also what happens if the user is not logged in. So in the previous test we did not test what happens when a user is not even logged in, until that test we always assumed the user was logged in with a role, now let's test when a user is not even logged in:

/** @test */
public function guest_user_should_get_redirected_to_login_when_viewing_admin_users_dashboard()
{
    // As we are guests, we directly access the URL, we do not need to do anything else
    $response = $this->get('/admin/users');

    $response->assertRedirect('/login');
}

See the test naming I have used, you should always use should instead of must, when instead of if.

Related