Using factory in PHPUnit provider fails
Asked Answered
D

6

16

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!

Drunk answered 17/7, 2017 at 8:57 Comment(1)
BTW, There are an unanswered questions like this in laracasts, this and thisDrunk
D
18

I found an answer in a different question (caused by the same issue)

So, this issue could be solved by calling $this->createApplication(); or $this->refreshApplication(); in the data provider method according to this answer, or by calling it in the constructor according to this answer

the code will look like this

public function dataProvider() {
    $this->refreshApplication(); // or $this->createApplication();
    $user = factory(\App\Model\User::class)->states('guest')->make();

    // ...
    // another code to generate data ....
    // ...

    return $someFakeData;
}

I tried that and worked, although I feel like its a workaround and not how things should work, any "cleaner" suggestions would be appreciated.

Drunk answered 17/7, 2017 at 15:37 Comment(1)
This is very bad solution because it's does not clear table after. Finishing test. I have noticed it then my other test have failed cause not empty table.Morganstein
L
15

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!

Leggat answered 14/5, 2020 at 21:14 Comment(0)
M
2

Alternatively, you could use Pest where factories just work in dataproviders.

it('can just use a factory in a data provider', function(YourModel $yourModel) {
    expect($yourModel)->toBeInstanceOf(YourModel::class);
})->with([
    [fn() => YourModel::factory()->create()],
]);
Motteo answered 3/2, 2023 at 9:37 Comment(0)
I
0

@Dan Fletcher

Brilliant solution (this one https://stackoverflow.com/a/61807494) !

The other solution where you have to call createApplication from the data provider didn't work for me (apart from being inefficient).

I think you could make the syntax slightly more palatable by using "shorthand closures", optionally combined with yield, e.g.

yield [ fn ($user) => $this->getData($user, 'subject', null, 'Subject is required.') ];

The $user argument to the closure would be the model object that you'd create via a Laravel Factory (that's the whole point, ultimately).

Interpellate answered 5/11, 2023 at 11:29 Comment(0)
G
0

You can try to pass "prepareFunction" as argument to test

public function dataProvider()
{
    return [
        [function() {
           SomeModel::factory()->create();
        }]
    ];
}

/** @dataProvider dataProvider */
public function testSomething($prepareFunction)
{
    $prepareFunction();
    
   // test here
}
Granddaughter answered 7/2 at 13:2 Comment(0)
H
-3

Do

dd(\App\Model\User::class);

To see if it returns right fully qualified class name, if it doesn't then that's probably your issue.

Hindmost answered 17/7, 2017 at 9:17 Comment(5)
it printed "App\Model\User"Drunk
Do you have that model? Is it in App\Model namespace?Hindmost
Thanks for your quick response, Yes, sure, I have that model (in App\Model namespace), and it worked when the factory() method was called from setup() or testSomeMethod() methods in the same class that contains the data provider methodDrunk
Just to me sure, are do you have parent::setUp(); in your controller or setUp() method?Hindmost
There are no controllers tested here, but, yes my setUp() method in my class that inherit TestCase class calls parent::setUp(); as first statement.Drunk

© 2022 - 2024 — McMap. All rights reserved.