Tests Failing
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'
);
}
}
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