FakerとファクトリでテストデータをリアルにするPHPテスト術
概要
PHPのテストにおける「テストデータの作り方」は、品質と保守性を大きく左右します。['name' => 'test', 'email' => 'a@a.com'] のようなベタ書きは書き始めこそ速いものの、カラムが増えるたびに全テストへ波及し、境界値のバグを取りこぼします。
本記事では Faker で現実味のあるダミーデータを生成し、ファクトリで「意味のある1件」を再利用可能な形に組み立てる方法をハンズオンで解説します。Laravelのモデルファクトリを主軸にしつつ、フレームワークに依存しない素のFaker+ファクトリパターンも扱います。
環境
- PHP 8.2+
fakerphp/faker1.x(Laravelには同梱)- Laravel 10/11 のモデルファクトリ(
database/factories) - PHPUnit / Pest どちらでも可
素のFakerを単体で使う場合は以下でインストールします。
composer require --dev fakerphp/faker
発生した問題
テストデータをベタ書きしていると、次の症状が出てきます。
- メンテコスト爆発: 必須カラムを1本追加しただけで、そのモデルを作る全テストが赤くなる。
- 非現実的なデータ:
email = "a@a.com"のような値ばかりで、バリデーションやフォーマット処理の穴を突けない。 - 重複衝突: ユニーク制約のあるカラムに同じ値を入れてしまい、テストが不安定になる。
- 意図が読めない: 「このテストが本当に効かせたい1カラム」が、大量のダミー値に埋もれて分からない。
原因
テストデータ生成に「値そのもの」と「テストの意図」が混在していることが原因です。本来、以下は分離できます。
- どうでもいい値(衝突せず・現実的でありさえすればいい)→ Fakerに任せる
- 意味のある値(このテストで検証したい状態)→ テスト側で上書きする
この分離ができると、テストは「何を検証したいか」だけを書けばよくなります。
対応方法
Fakerの基本
Fakerはロケール対応のダミーデータ生成器です。日本語ロケールを指定すれば、それらしい氏名・住所・電話番号が得られます。
use Faker\Factory;
$faker = Factory::create('ja_JP');
$faker->name(); // 例: 山田 太郎
$faker->safeEmail(); // 例: yamada.taro@example.org(予約ドメインで安全)
$faker->phoneNumber(); // 例: 090-1234-5678
$faker->address(); // それらしい住所
$faker->dateTimeBetween('-3 years', 'now');
$faker->randomElement(['pending', 'active', 'closed']);
ポイントは safeEmail() を使うこと。email() は実在しうるドメインを吐くため、うっかりメール送信テストで外部に飛ばさないよう、予約ドメイン(example.com 等)を返す safeEmail() が安全です。
ユニーク・オプショナルの修飾子
現実のデータは「必ずしも埋まっていない」「重複してはいけない」という性質を持ちます。Fakerはこれを修飾子で表現できます。
$faker->unique()->userName(); // ユニーク制約カラム向け(重複させない) $faker->optional(0.7)->phoneNumber(); // 70%の確率で値、30%はnull $faker->numberBetween(1, 100); $faker->boolean(30); // 30%の確率でtrue
unique() は同一Fakerインスタンス内で値の重複を避けます。大量生成でプールを使い切ると例外になるため、リセットしたいときは $faker->unique(true) を使います。
Laravelモデルファクトリ
Laravelではモデルごとに「現実的な1件のデフォルト」をファクトリに書きます。
// database/factories/UserFactory.php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => Hash::make('password'),
'status' => 'active',
'birth_date' => fake()->dateTimeBetween('-60 years', '-18 years'),
'created_at' => fake()->dateTimeThisYear(),
];
}
}
テスト側では「気にするカラムだけ」を上書きします。
$user = User::factory()->create(); // 全部おまかせ $admin = User::factory()->create(['status' => 'admin']); // 意図だけ明示 $users = User::factory()->count(20)->create(); // 20件まとめて
ステート(state)で意味のあるバリエーションを名前付けする
「退会済みユーザー」「未認証ユーザー」など、頻出する状態はステートとして名前を付けると意図が伝わります。
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
public function suspended(): static
{
return $this->state(fn () => ['status' => 'suspended']);
}
$user = User::factory()->unverified()->suspended()->create();
テストコードが unverified()->suspended() と読めるので、['email_verified_at' => null, 'status' => 'suspended'] より圧倒的に意図が明確です。
リレーションを含む「現実的なまとまり」を作る
現実のデータは単体で存在しません。ユーザーには投稿があり、投稿にはコメントがあります。ファクトリはこの関係ごと組み立てられます。
// 1ユーザーに投稿3件、各投稿にコメント5件
$user = User::factory()
->has(Post::factory()->count(3)
->has(Comment::factory()->count(5)))
->create();
// 逆方向: 投稿を作りつつ親ユーザーを指定
$post = Post::factory()->for($user)->create();
これで「N+1が起きるデータ」「集計対象が複数ある状態」など、単発では再現しづらいシナリオをテストできます。
実装例
境界値やバリデーションを突くテストの例です。Fakerで土台を作り、検証したい1点だけを上書きします。
use Tests\TestCase;
use App\Models\User;
class UserRegistrationTest extends TestCase
{
public function test_18歳未満は登録できない(): void
{
// 「未成年」という意図だけを明示、他は現実的なダミー
$payload = User::factory()->make([
'birth_date' => now()->subYears(17),
])->toArray();
$response = $this->postJson('/api/users', $payload);
$response->assertStatus(422)
->assertJsonValidationErrors(['birth_date']);
}
public function test_退会済みユーザーはログインできない(): void
{
$user = User::factory()->suspended()->create([
'password' => \Hash::make('secret-pass'),
]);
$response = $this->postJson('/api/login', [
'email' => $user->email,
'password' => 'secret-pass',
]);
$response->assertStatus(403);
}
}
make() はDBに保存せずインスタンスだけ生成し、create() は保存まで行います。バリデーションテストのように「保存前に弾く」検証では make() が向きます。
確認方法
- 再現性: テストごとにDBがリセットされる前提(
RefreshDatabaseトレイト等)で、factory()->create()が毎回成功することを確認する。 - シード固定: 特定のデータ組み合わせで落ちるバグを追うときは、Fakerのシードを固定して再現させる。
// setUp などで fake()->seed(1234); // 以降のfake()呼び出しが決定的になる
// 素のFakerの場合
$faker = \Faker\Factory::create('ja_JP');
$faker->seed(1234);
シードを固定すると、CIで「ときどき落ちる」フレーキーなテストの原因データを手元で再現できます。
注意点
- ユニーク制約に注意: メール等のユニークカラムは必ず
unique()を付ける。バルク生成でプール枯渇が起きたらリセットする。 - Fakerに検証対象を委ねない: テストで検証したい値は必ずテスト側で明示上書きする。ランダム値に判定を依存させると、たまたま通る/落ちるが発生する。
email()ではなくsafeEmail(): 予約ドメインを返すメソッドを使い、外部送信事故を防ぐ。- ロケールの選択: マルチバイトのバリデーション(氏名の文字数制限など)を検証したいなら
ja_JPを選ぶ。ASCIIしか出さないロケールだと日本語特有の境界を見逃す。 - 決定的テストとランダムのバランス: ランダムデータは「気づかなかった穴」を突く価値がある一方、再現性を損なう。落ちたらシード固定で追える体制を整えておく。
まとめ
- Fakerは「どうでもいい値」を現実的に埋め、ファクトリは「意味のある1件のデフォルト」を再利用可能にする。
- テストは検証したいカラムだけを上書きし、意図をコードに残す。
- ステートで状態に名前を付け、リレーションで現実的なまとまりを組み立てる。
- シード固定でフレーキーテストを再現可能にする。
「テストデータをリアルにする」とは、値を現実的にすることと、テストの意図を明確にすることの両輪です。Faker+ファクトリはその両方を同時に叶えます。
