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つの問題

  1. 非現実的すぎる — 名前が aaa、メールが a@a.com、住所が空。UI崩れやバリデーション漏れに気づけない。
  2. 再現できない — 特定環境でだけ再現するバグ。ランダムすぎて追えない。
  3. 投入が手作業 — 環境を作り直すたびに消える。

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 を整えておくと、以降の開発速度とレビューの質がはっきり変わります。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。