Ookma-Kyi's avatar

Test For Character Limit Reached

I'm building a test to limit the number of characters users can enter. The limit is set in the .env file which is then saved as a config setting and read by the test. I am receiving the following error:

ErrorException: Attempt to read property "id" on null
at vendor\laravel\framework\src\Illuminate\Foundation\Bootstrap\HandleExceptions.php:255
at database\factories\CharacterFactory.php:30
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:454
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:433
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:417
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Concerns\GuardsAttributes.php:155
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:422
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:390
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:284
at tests\Feature\Controllers\CharacterController\IndexTest.php:41
at vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestCase.php:177

After talking to Larry he suggested I check to make sure the database is seeded. Here is my test showing it being seeded at the very beginning as well as passing the instances into the recycle method:

it('Should allow the correct number of characters', function () {
    // Arrange
    $user = User::factory()->create();
    $belt = Belt::factory(3)->create();

    $counter = config("characters.max_allowed") + 1;

    Character::factory($counter)->recycle($user, $belt)->create();

    $this->actingAs($user)->
    get(route('characters.create'))
        ->assertInertia(fn (AssertableInertia $inertia) => $inertia
            ->component('Characters/MaxCharacters', true)
        );
});

Line 30 is related to the belt instances as shown:

<?php

namespace Database\Factories;

use App\Models\Belt;
use App\Models\Character;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends Factory<Character>
 */
class CharacterFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $xp = fake()->numberBetween(0 , 50);

        $belt = Belt::where('min_xp', '<=', $xp)->orderBy('min_xp', 'desc')->first();

        return [
            'user_id' => User::factory(),
            'name' => fake()->randomAscii(0, 25),
            'xp' => $xp,
            'belt_id' => $belt->id, // <-- line 30
            'wins' => fake()->numberBetween(0, 99),
            'loses' => fake()->numberBetween(0, 99),
            'draws' => fake()->numberBetween(0, 99)
        ];
    }
}

If the test passes it should display the MaxCharacters Vue instead of the Create Vue. Any ideas?

0 likes
11 replies
LaryAI's avatar
Level 58

It seems like the error you're encountering is due to the $belt variable being null when you're trying to access its id property. This can happen if there are no Belt records in the database that meet the condition specified in the where clause.

To resolve this issue, you should ensure that there are Belt records in the database before running the test, and that at least one of them will satisfy the condition min_xp <= $xp. Additionally, you should handle the case where no Belt is found to avoid the null reference error.

Here's how you can modify your CharacterFactory to handle the case where no Belt is found:

public function definition(): array
{
    $xp = fake()->numberBetween(0 , 50);

    $belt = Belt::where('min_xp', '<=', $xp)->orderBy('min_xp', 'desc')->first();

    // Check if a belt was found, if not, you can either create one or handle the error
    if (!$belt) {
        // Option 1: Create a default belt or throw an exception
        // $belt = Belt::factory()->create(['min_xp' => 0]); // Create a default belt
        // Option 2: Handle the error, for example by throwing an exception
        throw new \Exception('No belt found for the given XP');
    }

    return [
        'user_id' => User::factory(),
        'name' => fake()->randomAscii(0, 25),
        'xp' => $xp,
        'belt_id' => $belt->id, // Now it should not be null
        'wins' => fake()->numberBetween(0, 99),
        'loses' => fake()->numberBetween(0, 99),
        'draws' => fake()->numberBetween(0, 99)
    ];
}

In your test, you're creating three Belt instances, but you need to make sure that at least one of them will have a min_xp value that is less than or equal to the xp value generated in the CharacterFactory. If the Belt instances you're creating do not cover the range of xp values that CharacterFactory might generate, you'll run into the same issue.

Make sure your Belt factory creates belts with a range of min_xp values that cover the possible xp values generated by the CharacterFactory. If necessary, you can adjust the Belt factory or explicitly create a Belt with a min_xp of 0 to ensure there's always a belt available for any xp value.

Ookma-Kyi's avatar

@LaryAI Thank you, but I have already covered this inside the BeltFactory itself:

<?php

namespace Database\Factories;

use App\Models\Belt;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends Factory<Belt>
 */
class BeltFactory extends Factory
{
    // static variable to keep track if this is the 1st belt instance seeded
    protected static bool $firstInstance = true;

    // Static variable to keep track of the last max XP
    protected static int $lastMaxXp = 0;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // generate a random color for the belt
        $colorName = fake()->colorName();

        // if this is the first belt instance
        if(self::$firstInstance) {
            // Set the minimum XP to be 0
            $minXp = 0;

            // and set firstInstance to false for the next instance
            self::$firstInstance = false;
        } else {
            // Otherwise set the minimum XP to be one more than the last max XP
            $minXp = self::$lastMaxXp + 1;
        }

        // Generate a max XP that is greater than the min XP
        // For example, you can use a random number between min XP + 20 and min XP + 50
        $maxXp = $minXp + fake()->numberBetween(20, 50);

        // Update the last max XP for the next belt
        self::$lastMaxXp = $maxXp;

        return [
            'name' => $colorName,
            'image' => storage_path('app/public/belts/' . $colorName . '.png'),
            'min_xp' => $minXp,
            'max_xp' => $maxXp,
        ];
    }
}

Your solution does not resolve my issue.

Snapey's avatar

change this, does the error move?

$belt = Belt::where('min_xp', '<=', $xp)->orderBy('min_xp', 'desc')->firstOrFail();   //firstOrFail
Ookma-Kyi's avatar

@Snapey It moved:

C:\laragon\bin\php\php-8.1.27-Win32-vs16-x64\php.exe C:\laragon\www\Ookma-Kyi-Core\vendor\pestphp\pest\bin\pest --teamcity --configuration C:\laragon\www\Ookma-Kyi-Core\phpunit.xml

Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\Belt].
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:619
at database\factories\CharacterFactory.php:24
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:454
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:433
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:417
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Concerns\GuardsAttributes.php:155
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:422
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:390
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:284
at tests\Feature\Controllers\CharacterController\IndexTest.php:41
at vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestCase.php:177
kokoshneta's avatar

@Ookma-Kyi That means there are no records that have min_xp <= $xp.

What are the actual seeded min_xp values in the database? Are they lower than $xp?

Ookma-Kyi's avatar

@kokoshneta Yes there is:

Tests:

PS C:\laragon\www\Ookma-Kyi-Core> php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ that true is true                                                                                                                                                        0.02s  

   FAIL  Tests\Feature\Controllers\CharacterController\CreateTest
  ✓ it gives back successful redirect response for unauthenticated users for the characters.create page                                                                      0.38s  
  ✓ it can be accessed by verified user                                                                                                                                      1.05s  
  ✓ it Should return the correct component                                                                                                                                   0.05s  
  ⨯ it Should allow the correct number of characters                                                                                                                         0.08s  
{"id":1,"name":"RoyalBlue","image":"C:\laragon\www\Ookma-Kyi-Core\storage\app\/public\/belts\/RoyalBlue.png","min_xp":0,"max_xp":49,"created_at":"2024-03-11T17:16:31.000000Z","updated_at":"2024-03-11T17:16:31.000000Z"}
   PASS  Tests\Feature\Controllers\CharacterController\IndexTest
  ✓ it gives back successful redirect response for unauthenticated users for the tasks.show page                                                                             0.04s  
  ✓ it can be accessed by verified user                                                                                                                                      0.04s  
  ✓ it Should return the correct component                                                                                                                                   0.05s  
  ✓ it passes characters to the view                                                                                                                                         0.06s  

   WARN  Tests\Feature\Jetstream\ApiTokenPermissionsTest
  - api token permissions can be updated → API support is not enabled.                                                                                                       0.04s  

   PASS  Tests\Feature\Jetstream\AuthenticationTest
  ✓ login screen can be rendered                                                                                                                                             0.04s  
  ✓ users can authenticate using the login screen                                                                                                                            0.24s  
  ✓ users cannot authenticate with invalid password                                                                                                                          0.12s  

   PASS  Tests\Feature\Jetstream\BrowserSessionsTest
  ✓ other browser sessions can be logged out                                                                                                                                 0.22s  

   WARN  Tests\Feature\Jetstream\CreateApiTokenTest
  - api tokens can be created → API support is not enabled.                                                                                                                  0.03s  

   PASS  Tests\Feature\Jetstream\DeleteAccountTest
  ✓ user accounts can be deleted                                                                                                                                             0.14s  
  ✓ correct password must be provided before account can be deleted                                                                                                          0.30s  

   WARN  Tests\Feature\Jetstream\DeleteApiTokenTest
  - api tokens can be deleted → API support is not enabled.                                                                                                                  0.04s  

   WARN  Tests\Feature\Jetstream\EmailVerificationTest
  - email verification screen can be rendered → Email verification not enabled.                                                                                              0.03s  
  - email can be verified → Email verification not enabled.                                                                                                                  0.03s  
  - email can not verified with invalid hash → Email verification not enabled.                                                                                               0.03s  

   PASS  Tests\Feature\Jetstream\ExampleTest
  ✓ it returns a successful response                                                                                                                                         0.04s  

   PASS  Tests\Feature\Jetstream\PasswordConfirmationTest
  ✓ confirm password screen can be rendered                                                                                                                                  0.04s  
  ✓ password can be confirmed                                                                                                                                                0.14s  
  ✓ password is not confirmed with invalid password                                                                                                                          0.26s  

   PASS  Tests\Feature\Jetstream\PasswordResetTest
  ✓ reset password link screen can be rendered                                                                                                                               0.05s  
  ✓ reset password link can be requested                                                                                                                                     0.09s  
  ✓ reset password screen can be rendered                                                                                                                                    0.07s  
  ✓ password can be reset with valid token                                                                                                                                   0.06s  

   PASS  Tests\Feature\Jetstream\ProfileInformationTest
  ✓ profile information can be updated                                                                                                                                       0.06s  

   WARN  Tests\Feature\Jetstream\RegistrationTest
  ✓ registration screen can be rendered                                                                                                                                      0.04s  
  - registration screen cannot be rendered if support is disabled → Registration support is enabled.                                                                         0.03s  
  ✓ new users can register                                                                                                                                                   0.06s  

   PASS  Tests\Feature\Jetstream\TwoFactorAuthenticationSettingsTest
  ✓ two factor authentication can be enabled                                                                                                                                 0.06s  
  ✓ recovery codes can be regenerated                                                                                                                                        0.05s  
  ✓ two factor authentication can be disabled                                                                                                                                0.05s  

   PASS  Tests\Feature\Jetstream\UpdatePasswordTest
  ✓ password can be updated                                                                                                                                                  0.15s  
  ✓ current password must be correct                                                                                                                                         0.19s  
  ✓ new passwords must match                                                                                                                                                 0.19s  
  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  
   FAILED  Tests\Feature\Controllers\CharacterController\CreateTest > it Should allow the correct number of characters                                     ModelNotFoundException   
  No query results for model [App\Models\Belt].

  at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:619
    615▕         if (! is_null($model = $this->first($columns))) {
    616▕             return $model;
    617▕         }
    618▕
  ➜ 619▕         throw (new ModelNotFoundException)->setModel(get_class($this->model));
    620▕     }
    621▕
    622▕     /**
    623▕      * Execute the query and get the first result or call a callback.

  1   vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:619
  2   database\factories\CharacterFactory.php:25


  Tests:    1 failed, 7 skipped, 31 passed (62 assertions)
  Duration: 5.21s

PS C:\laragon\www\Ookma-Kyi-Core>

This line shows that the belt exists:

{"id":1,"name":"RoyalBlue","image":"C:\laragon\www\Ookma-Kyi-Core\storage\app\/public\/belts\/RoyalBlue.png","min_xp":0,"max_xp":49,"created_at":"2024-03-11T17:16:31.000000Z","updated_at":"2024-03-11T17:16:31.000000Z"}
   PASS  Tests\Feature\Controllers\CharacterController\IndexTest
kokoshneta's avatar

@Ookma-Kyi But at the same time, down at the end, there’s a stack trace showing that line 25 in your CharacterFactory tries to load a model that doesn’t exist, and that is the line that has Belt::where(…)->first(). I can’t see from the code in the question where the JSON output of the Belt model is coming from, but at the time when your CharacterFactory is created and tries to load a Belt model, there is no such model.

Can you try outputting the actual query sent to the database to see what it is?

Ookma-Kyi's avatar

Dumb question, do the tests automatically seed the database, cause looking at the database directly shows empty tables?

Ookma-Kyi's avatar

@Snapey I'm using LazilyRefreshDatabase as per the "Build a web forum in Laravel" series.

Please or to participate in this conversation.