0

So I am working on some unit testing before I implement a new feature. I run my test and it fails with OverflowException : Maximum retries of 10000 reached without finding a unique value This is the test I'm running.

    public function test_job_factory()
    {
        $client = Client::factory()->create();
        $accountHandler = AccountHandler::factory()->create();
        $user = User::factory()->create();

        $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        $user->givePermissionTo( 'manage jobs' );
        $clientContact = ClientContact::factory()->create();
        $job = Job::factory()->create();

        $this->assertTrue($job->id > 0);
    }

The error seems to happen while creating the job itself. Tests above this test the other factories and are working.

Heres the JobFactory.php file:

<?php

namespace Database\Factories;

use App\Models\AccountHandler;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Job;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Job>
 */
class JobFactory extends Factory
{
    protected $model = Job::class;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'title'              => $this->faker->company(),
            'is_active'          => 1,
            'is_draft'           => 0,
            'client_id'          => $this->faker->unique()->numberBetween(1, Client::count()),
            'account_handler_id' => $this->faker->unique()->numberBetween(1, AccountHandler::count()),
            'eclipse_contact_id' => $this->faker->unique()->numberBetween(1, User::count()),
            'client_contact_id'  => $this->faker->unique()->numberBetween(1, ClientContact::count()),
            'description'        => $this->faker->paragraphs(1),
        ];
    }
}

and the migration for this (create_jobs_table.php):

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('jobs', function (Blueprint $table) {
            $table->id('number');
            $table->boolean( 'is_active' )->default(true);
            $table->boolean( 'is_complete' )->default(true);
            $table->string('title', 64)->nullable();
            $table->timestamps();
            $table->foreignId('author')->nullable()->constrained()->references('id')->on('users');
            $table->text('description')->nullable();
            $table->foreignId('client_id')->nullable()->constrained()->references('id')->on('clients');
            $table->foreignId('client_contact_id')->nullable()->constrained()->references('id')->on('client_contacts');
            $table->foreignId('account_handler_id')->nullable()->constrained()->references('id')->on('account_handlers');
            $table->date('expiry_date')->nullable();
            $table->date('artwork_deadline')->nullable();
            $table->date('proof_deadline')->nullable();
            $table->integer('redirect')->nullable();
            $table->boolean( 'is_draft' )->default(true);
            $table->foreignId('eclipse_contact_id')->nullable()->constrained()->references('id')->on('users');
            $table->foreignId('subscription_id')->nullable()->constrained()->references('id')->on('subscriptions');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('jobs');
    }
};

So what is wrong and why this loop here? I have added the minimum data that is needed to create a job so no idea what I've missed.

thanks

*** EDIT *** I was asked to give a link to where I found the bad practice of using unique and numberBetween here is an example Laravel factory error "Maximum retries of 10000 reached without finding a unique value" when I use unique Faker method this will not work as you expect!

ThurstonLevi
  • 664
  • 13
  • 34
  • 2
    What's the reason behind using `$this->faker->unique()->numberBetween(1, Client::count())` instead of just creating a new `Client` i.e. `Client::factory()`? The same question for the other relationships. – Rwd Jun 29 '22 at 15:02
  • 2
    Why do you need `->unique()`? Also, instead of `numberBetween(1, N)`, why not `-> randomElement(Client::pluck('id'))`? That's _safer_ than relying on `1-to-count()` to represent available ids... (or using `Model::factory()` as suggested above) – Tim Lewis Jun 29 '22 at 15:03
  • Thanks guys. That has now got it!. Reason for the numberBetween is I'd looked on a few posts on here and that was one suggestion. In my scenario it made sense (I thought) as the id would always start at one. I do however see it as being a bad idea now. Say i created 2 clients and deleted one and then added a new one then count would be 2 but id's would be 1 and 3 and if instead removed the first client then id's would be 2 and 3. plenty of scope for the kind of errors which are hard to trace. using randomElement gets around that perfectly. – ThurstonLevi Jun 30 '22 at 07:24
  • Please link those posts so that the answers can be marked as not helpful if appropriate. – hakre Jun 30 '22 at 08:12
  • updated question with reference example of bad practise – ThurstonLevi Jul 01 '22 at 08:27

2 Answers2

1

I will recommend to follow documentation approach for this problem. https://laravel.com/docs/9.x/database-testing#factory-relationships

Looking at your migration table, there is a big chance that your factory will not work, since you indicating the relation on SQL level, which basically will force you to have records in related table, otherwise SQL will throw you with an error..

  • Oh thats interesting. I will try this way on my next tests. as up until now I've only been adding a single factory model, I have plenty of tests that do need multiple elements. As is always with the docs how on earth did I not see that section! I ended up using suggestion above by Tim Lewis to use $this->faker->randomElement(Model::pluck('id')) which allowed this test to pass. – ThurstonLevi Jun 30 '22 at 07:33
0

The problem is that when you try and get 5 values between 1 and 5 (including) and you can only fetch 4. The 5th number will never pass the unique() validation.

its because when you run the first instance of unique, for instance

$faker->unique()->numberBetween(1, 20)

then run it in another factory, laravel usually extends the previous instance(i guess), if that makes sense. But when you pass true like below

$faker->unique(true)->numberBetween(1, 20)

it starts searching from 1 to 20 again

this solved the problem for me.

so always pass true in the unique() instace

SEYED BABAK ASHRAFI
  • 4,093
  • 4
  • 22
  • 32
young
  • 1
  • 1