FakerとファクトリでテストデータをリアルにするPHPテスト術

概要

PHPのテストにおける「テストデータの作り方」は、品質と保守性を大きく左右します。['name' => 'test', 'email' => 'a@a.com'] のようなベタ書きは書き始めこそ速いものの、カラムが増えるたびに全テストへ波及し、境界値のバグを取りこぼします。

本記事では Faker で現実味のあるダミーデータを生成し、ファクトリで「意味のある1件」を再利用可能な形に組み立てる方法をハンズオンで解説します。Laravelのモデルファクトリを主軸にしつつ、フレームワークに依存しない素のFaker+ファクトリパターンも扱います。

環境

  • PHP 8.2+
  • fakerphp/faker 1.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+ファクトリはその両方を同時に叶えます。

\ 最新情報をチェック /

コメントを残す

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