Laravel のテストで data provider の中で faker を使うとエラーになる
長いタイトルになりましたが、タイトル通りです。エラーメッセージは InvalidArgumentException: Unknown format "name"
です。
環境
Laravel 9 で確認しましたが、10, 11 でも同様だと思います。
現象
エラーが起きるコード
実際のコードを少し簡略化して載せます。
テストケースはこんな感じです。
// テストケース
use Tests\TestCase;
class AccountTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// ここで本テストケース固有の初期化処理を実行するが、詳細は省略
}
// 問題となる data provider
public function provider_user()
{
return [
'User without foo' => [$this->getCustomer()],
'User with foo' => [$this->getCustomerWithFoo()],
];
}
// テスト内で使われるユーティリティメソッド1
private function getCustomer($overrideUser = [])
{
// ファクトリーを呼び出し
return Customer::factory($overrideUser)
->create();
}
// テスト内で使われるユーティリティメソッド2
private function getCustomerWithFoo($overrideUser = [], $overrideFoo = [])
{
// ファクトリーを呼び出し
return Customer::factory($overrideUser)
->has(Foo::factory($overrideFoo)->count(1))
->create();
}
/**
* テストケース
* @dataProvider provider_user
* @return void
*/
public function test_some_behavior($user)
{
$response = $this->actingAs($user)->get('/some/page');
$response->assertStatus(200);
}
}
ファクトリーは以下の通りです。
use Illuminate\Database\Eloquent\Factories\Factory;
class CustomerFactory extends Factory
{
public function definition()
{
// faker を使ってます。
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
}
エラーの内容
こんな感じのエラーが出ます。
• PHPUnit\Framework\ErrorTestCase > error
PHPUnit\Framework\Error
The data provider specified for Tests\Feature\Customer\AccountTest::test_some_behavior is invalid.
InvalidArgumentException: Unknown format "name"
/var/www/html/vendor/fakerphp/faker/src/Faker/Generator.php:731
/var/www/html/vendor/fakerphp/faker/src/Faker/Generator.php:696
/var/www/html/vendor/fakerphp/faker/src/Faker/Generator.php:961
/var/www/html/database/factories/CustomerFactory.php:19
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:429
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:408
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:392
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:155
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:397
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:365
/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:265
/var/www/html/tests/Feature/Customer/AccountTest.php:43
at vendor/phpunit/phpunit/phpunit:98
94▕ unset($options); 95▕
96▕ require PHPUNIT_COMPOSER_INSTALL; 97▕
➜ 98▕ PHPUnit\TextUI\Command::main();
99▕
どうすれば良いか
テストケースを以下のように修正すれば良いです。変更点のみを記載します。
// data provider
public function provider_customer_creation_method_name()
{
// $this->getCustomer(), getCustomerWithFoo() を呼び出してその値を返すのでは無く
// メソッド名を返すように変更。
return [
'User without foo' => ['getCustomer'],
'User with foo' => ['getCustomerWithFoo'],
];
}
/**
* テストケース
* @dataProvider provider_customer_creation_method_name
* @return void
*/
public function test_some_behavior($method)
{
$user = call_user_func([$this, $method]); // 変更点
$response = $this->actingAs($user)->get('/customer/account/show');
$response->assertStatus(200);
}
同僚に本記事をレビューしてもらった際、以下のように closure を使った方法を教えてもらいました。こちらでも問題ありません。
// data provider
public function provider_customer_creation_closure()
{
// $this->getCustomer(), getCustomerWithFoo() を直接呼び出しすのでは無く
// closure 内で呼び出すように変更。
return [
'User without foo' => [fn() => static::getCustomer()],
'User with foo' => [fn() => static::getCustomerWithFoo()],
];
}
/**
* テストケース
* @dataProvider provider_customer_creation_closure
* @return void
*/
public function test_some_behavior($createUserClosure)
{
$user = $createUserClosure(); // 変更点
$response = $this->actingAs($user)->get('/customer/account/show');
$response->assertStatus(200);
}
どちらも出来る事としては変わらないと思います。
解説
data provider は、テストケースの初期化・実行より前に評価されます。 Testing\TestCase::setUp
の中で faker の初期化も含めた各種初期化処理が行われるため、それより前に faker を使おうとするとエラーとなります。
それを回避するためには、data provider の中で faker を使ったファクトリーを呼び出すのでは無く、テストケース内で呼び出すようにします。
ちなみに、PHP に遅延評価の仕組みがあればもっと簡単に解決できると思います。
まとめ
Laravel のテストで data provider の中で faker を使うとエラーになります。data provider の中で faker を使うケースというのは、data provider の中で faker を使ったファクトリーを呼び出す場合が主だと思います。それを回避するためには、data provider の中ではなくテストケースの中でファクトリーを呼び出すようにします。
コメントを残す