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!
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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With