Using Eloquent Factories With PHPUnit Data Providers
Published on by Paul Redmond
There are a few ways to work with Laravel's factories in feature tests, such as creating a model during setUp()
when you want to use it for multiple tests or directly in an individual test case. If you have a test case that you want to test against a variety of data, you might want to reach for PHPUnit's data providers with Eloquent models.
Using data providers with feature tests can pose a problem because they run before Laravel is bootstrapped via the framework's TestCase
that runs during setUp()
. Data providers are resolved early in the process of running phpunit
, so you'll run into the following error if you want to use them:
<?php namespace Tests\Feature; use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use PHPUnit\Framework\Attributes\DataProvider;use Tests\TestCase; class ExampleTest extends TestCase{ use RefreshDatabase; #[DataProvider('nonAdminUsers')] public function test_non_admin_users_cannot_access_admin($user): void { $response = $this ->actingAs($user()) ->get('/admin') ->assertStatus(403); } public static function nonAdminUsers(): array { return [ [User::factory()->player()->create()], [User::factory()->coach()->create()], [User::factory()->owner()->create()], ]; }}
If you run the above test, you should get something like the following error, depending on which version of Laravel you are using—the following is what I get on Laravel 11:
$ vendor/bin/phpunit tests/Feature/ExampleTest.php There was 1 PHPUnit error: 1) Tests\Feature\ExampleTest::test_non_admin_users_cannot_access_adminThe data provider specified for Tests\Feature\ExampleTest::test_non_admin_users_cannot_access_admin is invalidA facade root has not been set. tests/Feature/ExampleTest.php:18
This is because when the data provider code runs, the Laravel app hasn't been bootstrapped! If you're a Pest PHP user, the Bound Datasets example illustrates using a closure for model data:
it('can generate the full name of a user', function (User $user) { expect($user->full_name)->toBe("{$user->first_name} {$user->last_name}");})->with([ fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']), fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']), fn() => User::factory()->create(['first_name' => 'Freek', 'last_name' => 'Van Der Herten']),]);
In PHPUnit, we could use closures to pass code to our test via data providers without immediately trying to create the data:
namespace Tests\Feature; use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use PHPUnit\Framework\Attributes\DataProvider;use Tests\TestCase; class ExampleTest extends TestCase{ use RefreshDatabase; #[DataProvider('nonAdminUsers')] public function test_non_admin_users_cannot_access_admin($user): void { $response = $this ->actingAs($user()) ->get('/admin') ->assertStatus(403); } public static function nonAdminUsers(): array { return [ [fn(): User => User::factory()->player()->create()], [fn(): User => User::factory()->coach()->create()], [fn(): User => User::factory()->owner()->create()], ]; }}
Note the $user()
call, which we pass to actingAs()
. If you need to use the model in various places, just assign it to a variable. Now, factory data is created in the test, which is precisely what we want! To learn more about HTTP feature tests in Laravel, check out the documentation.