PLATFORM
  • Tails

    Create websites with TailwindCSS

  • Blocks

    Design blocks for your website

  • Wave

    Start building the next great SAAS

  • Pines

    Alpine & Tailwind UI Library

  • Auth

    Plug'n Play Authentication for Laravel

  • Designer comingsoon

    Create website designs with AI

  • DevBlog comingsoon

    Blog platform for developers

  • Static

    Build a simple static website

  • SaaS Adventure

    21-day program to build a SAAS

Question By
Unsolved
ookma-kyi

Mar 11th, 2024 10:49 AM

I am creating a test to verify that only the max allowed amount of characters are created. However whenever I run the test I get the following error:

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:25
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:400
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:401
at vendor\laravel\framework\src\Illuminate\Database\Eloquent\Factories\Factory.php:284
at tests\Feature\Controllers\CharacterController\CreateTest.php:44
at vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestCase.php:177

This test was ignored.

This test was ignored.

This test was ignored.

This test was ignored.

This test was ignored.

This test was ignored.

This test was ignored.

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


Process finished with exit code 2

It is referencing this test:

it('passes characters to the view', function () {
    $user = User::factory()->create();
    // Seed the database with 6 belt instances
    Belt::factory(6)->create();

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

    $this->actingAs($user)->
    get(route('characters.index', ['user' => $user]))
        ->assertInertia(fn (AssertableInertia $inertia) => $inertia
            ->has('characters')
        );
});

However if I comment out Belt::factory(6)->create(); the line it changes to the same error on this test:

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

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

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

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

Accordingly if I comment out the same line on the 1st test, it produces the same error, but moves to the previous test. If I comment out both lines it produces the same error for both tests.

Here is my belt factory:

<?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,
        ];
    }
}

CharacterFactory:

<?php

namespace Database\Factories;

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

/**
 * @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')->firstOrFail();   //firstOrFail

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

UserFactory:

<?php

namespace Database\Factories;

use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Laravel\Jetstream\Features;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'two_factor_secret' => null,
            'two_factor_recovery_codes' => null,
            'remember_token' => Str::random(10),
            'profile_photo_path' => null,
            'current_team_id' => null,
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(function (array $attributes) {
            return [
                'email_verified_at' => null,
            ];
        });
    }

    /**
     * Indicate that the user should have a personal team.
     */
    public function withPersonalTeam(callable $callback = null): static
    {
        if (! Features::hasTeamFeatures()) {
            return $this->state([]);
        }

        return $this->has(
            Team::factory()
                ->state(fn (array $attributes, User $user) => [
                    'name' => $user->name.'\'s Team',
                    'user_id' => $user->id,
                    'personal_team' => true,
                ])
                ->when(is_callable($callback), $callback),
            'ownedTeams'
        );
    }
}
bobbyiliev

Mar 11th, 2024 11:14 AM

Hey!

The error message you're encountering, Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\Belt], indicates that your test is attempting to find a Belt model instance that does not exist in the database at the point it's being queried.

This issue is happening in your CharacterFactory, specifically in this line:

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

When this line is executed, it expects at least one Belt instance in the database with a min_xp value less than or equal to the generated $xp. If no such Belt instance is found, a ModelNotFoundException is thrown, which is what you're seeing.

Given that commenting out Belt::factory(6)->create(); in your tests leads to the same error, it seems like the creation of Belt instances is not happening as expected, or the Belt instances are not meeting the condition required in your CharacterFactory.

In your CharacterFactory, you're generating a character's XP with $xp = fake()->numberBetween(0 , 50);. Make sure that this range aligns with the min_xp and max_xp ranges being set in your BeltFactory. If no belts are within the range of 0 to 50 XP, your query will fail.

Here are a few things that you can do to troubleshoot and fix this issue:

1. Ensure Belt Instances Creation:

Verify that the Belt instances are indeed being created in the database during the test execution. You can add a debugging statement right after you attempt to create the belts to confirm their creation.

Belt::factory(6)->create();
$beltsCount = Belt::count();
Log::info("Number of belts created: {$beltsCount}");

If the log shows that belts are not being created, you'll know there's an issue with the BeltFactory.

2. Adjust XP Generation Logic:

You might want to ensure that the XP generated for a Character always falls within the range defined by the existing belts. For this, consider adjusting the XP generation logic to pick a random value within the range of existing belts.

$minXp = Belt::min('min_xp') ?? 0;
$maxXp = Belt::max('max_xp') ?? 50;
$xp = fake()->numberBetween($minXp, $maxXp);

This ensures that the generated xp for a Character is always within the bounds of the existing Belt instances.

3. Check Belts Overlap:

Ensure that the belts' XP ranges do not overlap and cover the entire range from 0 to the maximum XP you're considering. You can add a test or a check after creating the belts to verify this.

$belts = Belt::orderBy('min_xp', 'asc')->get();
$previousMaxXp = -1;
foreach ($belts as $belt) {
    if ($belt->min_xp <= $previousMaxXp) {
        Log::error("Overlapping or incorrect XP range detected for belt: {$belt->name}");
    }
    $previousMaxXp = $belt->max_xp;
}

This will log an error if there's any overlapping or incorrect XP range among the belts.

4. Explicitly Set Belt IDs in CharacterFactory:

Instead of letting the CharacterFactory determine which belt to assign based on XP, you can explicitly assign a belt ID. This can be particularly useful for testing specific scenarios.

public function definition(): array {
    $belt = Belt::inRandomOrder()->firstOrFail();
    $xp = fake()->numberBetween($belt->min_xp, $belt->max_xp);

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

Let me know how it goes!

Best,

Bobby