Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using factory in PHPUnit provider fails

I'm trying to use a model factory to make models in a data provider. It works if i use the factory in the setup method or in the test directly, but if I try to use it in a data provider I get an error:

1) Warning

The data provider specified for MyClassTest::testSomeMethod is invalid.

Unable to locate factory with name [default] [App\Model\User].

The factory definition:

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(\App\Model\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'id' => $faker->unique()->numberBetween(1, 10000),
        'email_address' => $faker->unique()->safeEmail,
        'remember_token' => str_random(10),
        'password' => $password ?: $password = bcrypt('secret'),
    ];
});

$factory->state(\App\Model\User::class, 'guest', function ($faker) {
    return [
        'role_id' => 9999,
    ];
});

My call to the factory:

factory(\App\Model\User::class)->states('guest')->make();

is it a bug from Laravel or am I missing something here?

Edit:

After some debugging, I found that factory definitions are not loaded before the data provider call, they where called (i.e. defined) when the setUp() method was called - which happens after data provider call -, so it can't find the factory in data provider.

So it seems to me that it is impossible to use factories in data providers (or any static method in the test class). Or there would be something I should do to define the factories in my data provider method!

like image 221
Alaeddin AL Zeybek Avatar asked Jul 17 '17 08:07

Alaeddin AL Zeybek


1 Answers

It's possible to use factories inside your data providers and still have database transactions work too!

This was frustrating me today and I figured out a solution inspired by this answer which I found thanks to this answer

It's not pretty but here it is:

Update I also turned this into a blog post which goes a bit more into detail: https://technicallyfletch.com/how-to-use-laravel-factories-inside-a-data-provider/

First I modified the way I consume the provider. Instead of expecting a list of arguments as I normally do, I expect a function from which I can destructure the arguments out of. This is what defers the execution of the factories until after I'm inside my test case.

    /**
     * @test
     * @dataProvider validationProvider
     */
    public function it_validates_payload($getData)
    {
        // data provider now returns a function which we can invoke to
        // destructure our arguments here.
        [$ruleName, $payload] = $getData();

        $this->post(route('participants.store'), $payload)
            ->assertSessionHasErrors($ruleName);
    }

My provider now becomes something like this:

    public function validationProvider()
    {
        return [
            'it fails if participant_type_id does not exist' => [
                function () {
                    return [
                        'participant_type_id',
                        array_merge($this->getValidData(), ['participant_type_id' => null])
                    ];
                }
            ],
            'it fails if first_name does not exist' => [
                function () {
                    return [
                        'first_name',
                        array_merge($this->getValidData(), ['first_name' => null])
                    ];
                }
            ],
            'it fails if last_name does not exist' => [
                function () {
                    return [
                        'last_name',
                        array_merge($this->getValidData(), ['last_name' => null])
                    ];
                }
            ],
            'it fails if email is not unique' => [
                function () {
                    $existingEmail = factory(Participant::class)->create([
                        'email' => '[email protected]'
                    ])->email;
                    return [
                        'email',
                        array_merge($this->getValidData(), ['email' => $existingEmail])
                    ];
                }
            ],
        ];
    }

And then this is sort of beside the point but it illustrates well that you can defer the factories. The getValidData() method is just returning an array of valid data so each test is only asserting one validation error at a time. It too, uses a factory:

    private function getValidData()
    {
        return [
            'practice_id' => factory(Practice::class)->create()->id,
            'participant_type_id' => 1,
            'first_name' => 'John',
            'last_name' => 'Doe',
        ];
    }

Some things that still bug me about this

  1. It's just sloppy looking. Data providers are already difficult to make readable and this just makes it a lot worse. Although you could abstract a utility to help clean this up.
  2. In my example, I have a database call that gets made for every single scenario, since it gets run with each execution of the provider's returned closures... yuk! I'm not sure what the best approach would be to fix that but one way would be to set up some state in the constructor of the class itself. Once the id is created after the first execution of the provider, you could pull the id from state rather than making a new call to the database each time.

Otherwise, it is a working solution for when you need this, and I hope it helps others if they happen to find this!

like image 74
Dan Fletcher Avatar answered Oct 24 '22 14:10

Dan Fletcher