Fakerで作る現実的なテストデータとSeeder活用術
概要
開発やテストにおいて、テストデータの質は作業効率に直結します。id が連番で name が test1、test2... のようなデータでは、画面のレイアウト崩れにも気づけず、検索やソートの挙動確認もままなりません。
この記事では、PHP の Faker(fakerphp/faker)を使って現実的なテストデータを生成し、Laravel の Factory / Seeder に組み込んで再現性のあるデータ投入を行う手順を、ハンズオン形式でまとめます。Laravel を題材にしますが、Faker 単体の考え方は他環境でも通用します。
環境
- PHP 8.3+(推奨 8.4)
- Laravel 13.x
- fakerphp/faker(Laravel には開発依存として標準同梱)
Laravel 13 は最低要件が PHP 8.3 以上です。古い PHP 8.2 のままでは動かないため、環境を合わせておきます。
Laravel を使っていれば Faker は最初から入っているため、追加インストールは不要です。単体で使う場合のみ以下を実行します。
composer require --dev fakerphp/faker
テストデータでありがちな3つの問題
- 非現実的すぎる — 名前が
aaa、メールがa@a.com、住所が空。UI崩れやバリデーション漏れに気づけない。 - 再現できない — 特定環境でだけ再現するバグ。ランダムすぎて追えない。
- 投入が手作業 — 環境を作り直すたびに消える。
Faker は 1 を、seed() による乱数固定は 2 を、Seeder は 3 を解決します。
Faker の基本
Faker\Factory::create() でジェネレータを作り、プロパティのように呼び出します。ロケールに ja_JP を渡すと、名前・住所・会社名などが日本語になり、実データに近い見た目になります。
<?php
use Faker\Factory;
$faker = Factory::create('ja_JP');
echo $faker->name; // 山田 太郎
echo $faker->safeEmail; // example@example.org(実在しない安全なドメイン)
echo $faker->phoneNumber; // 090-1234-5678
echo $faker->company; // 株式会社○○
echo $faker->realText(100); // それっぽい日本語の文章
よく使うメソッドを整理します。
| 用途 | メソッド |
|---|---|
| 氏名 | name / firstName / lastName |
| メール | safeEmail / unique()->safeEmail |
| 日付 | dateTimeBetween('-1 year', 'now') |
| 数値 | numberBetween(1, 100) |
| 選択肢 | randomElement(['A', 'B', 'C']) |
| 真偽 | boolean(70)(70%でtrue) |
| 任意null | optional()->phoneNumber(たまにnull) |
「揺らぎ」を現実に寄せる
テストデータが現実的かどうかは「揺らぎ」で決まります。
unique()->safeEmail— ユニーク制約のあるカラムは重複を避けるoptional(0.3)->phoneNumber— 30%の確率で値、70%でnull。必須でない欄のnullを再現する
null が混ざることで、表示側の「未入力時の分岐」を自然にテストできます。
Factory に組み込む
config/app.php でロケールを設定します。なお Laravel 11 以降のスリム化された config では faker_locale が config/app.php に既定で含まれません。キーが無ければ以下のように自分で追記します。
// config/app.php 'faker_locale' => 'ja_JP',
Factory では $this->faker を使います。
// database/factories/UserFactory.php
<?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' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'phone' => $this->faker->optional(0.6)->phoneNumber(),
'password' => Hash::make('password'),
'role' => $this->faker->randomElement(['admin', 'staff', 'member']),
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
];
}
public function admin(): static
{
return $this->state(fn (array $attributes) => ['role' => 'admin']);
}
}
state(admin())を定義すると、呼び出し側の意図が読みやすくなります。
User::factory()->count(10)->create(); // 一般10件 User::factory()->count(3)->admin()->create(); // 管理者3件
リレーションごと生成する
一覧画面や N+1 の確認には、関連データごと生成できると便利です。
use App\Models\User;
use App\Models\Post;
User::factory()
->count(5)
->has(Post::factory()->count(3)) // 各ユーザーに3件の投稿
->create();
Post::factory()
->count(10)
->for(User::factory()->admin()) // すべて同じ管理者に紐づく
->create();
PostFactory では親キーを User::factory() にしておけば、単独 create() 時に親も自動生成されます。
Seeder でシナリオを組む
Factory が「作り方」、Seeder が「どれをどれだけ入れるか」です。
// database/seeders/DatabaseSeeder.php
public function run(): void
{
User::factory()->admin()->create([
'name' => '管理者',
'email' => 'admin@example.com',
]);
User::factory()->count(20)->has(Post::factory()->count(5))->create();
}
php artisan db:seed # Seedだけ php artisan migrate:fresh --seed # 作り直してSeed
種別ごとに Seeder を分け、call() でまとめると見通しがよくなります。
$this->call([UserSeeder::class, PostSeeder::class]);
再現性: 乱数を固定する
seed() を呼ぶと生成が決定的になり、「ランダムだけど毎回同じ」になります。再現バグをチームで同条件で追えます。
$faker = Factory::create('ja_JP');
$faker->seed(1234);
テストでは RefreshDatabase と併用します。
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_一覧が表示される(): void
{
$this->faker->seed(42);
$posts = Post::factory()->count(10)->create();
$this->get('/posts')
->assertOk()
->assertSeeText($posts->first()->title);
}
}
大量データはチャンク INSERT
数万件を create() で回すと遅くなります。属性だけ生成してまとめて投入します。
use Illuminate\Support\Facades\DB;
$now = now();
collect(range(1, 50000))
->chunk(1000)
->each(function ($chunk) use ($now) {
$rows = $chunk->map(fn () => [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('users')->insert($rows);
});
DB::table()->insert() はタイムスタンプを自動付与しないため手動で埋めます。1000件程度のチャンクに区切り、メモリと unique() 候補の枯渇を避けます。
確認方法
// php artisan tinker
>>> User::count();
=> 21
>>> User::first()->name;
=> "山田 太郎"
>>> User::pluck('email')->duplicates()->isEmpty();
=> true
注意点
- 本番環境で Seeder を流さない。
app()->environment('production')でガードする。 emailでなくsafeEmailを使う。前者は実在ドメインを生成しうるため、誤送信防止に予約ドメインを使う。unique()の枯渇。選択肢の少ないメソッドにunique()を付けて大量生成すると例外になる。候補の広いメソッドと併用する。- ロケール依存の挙動。
phoneNumberの桁数などはロケールで変わる。厳密にアサートするなら固定値も検討する。
まとめ
- Faker + ロケールで現実的な見た目を、
unique()/optional()で揺らぎを作る。 - Factory は「作り方」、Seeder は「シナリオ」。state で種別を宣言的に表現する。
has()/for()でリレーションごと生成でき、一覧画面や N+1 の確認に使える。seed()で乱数を固定すれば、ランダムなのに再現可能になる。- 大量データは
DB::table()->insert()のチャンク投入で高速化する。 - 本番での Seed 実行はガードする。
テストデータの整備は地味ですが、最初に Factory を整えておくと、以降の開発速度とレビューの質がはっきり変わります。
