【Laravel】FactoryとRefreshDatabaseでテストデータを整える

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はUnit Testで、DBに依存しない単体ロジックのテスト方法を解説しました。

今回は、DBを使うテストを書くうえで欠かせない「Factory」と「RefreshDatabase」を解説します。

これでテスト編の基礎パートは一区切りです。

DBを使うテストで起きがちな問題

テストごとにデータが残ってしまう

Feature TestやUnit Testの中には、実際にDBへ保存されたデータを使って検証したいケースがあります。

このとき困るのが「前のテストで作ったデータが、次のテストにも残ってしまう」という問題です。

私が最初にこれにハマったときは、「投稿件数が0件であること」を検証するテストが、別のテストで作った投稿データのせいで失敗し続け、原因が分からず1時間近く悩んだことがあります。

テストは、実行するたびに同じ結果になる「独立した状態」で走らせる必要があります。

RefreshDatabaseで毎回まっさらな状態にする

この問題を解決するのがRefreshDatabaseトレイトです。

<?php
// tests/Feature/PostControllerTest.php
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_投稿が0件のとき一覧は空になる(): void
    {
        $response = $this->get('/posts');

        $response->assertSee('投稿はまだありません');
    }
}

use RefreshDatabase;を1行加えるだけで、各テストの実行前にマイグレーションを実行し直し、テストごとにDBをまっさらな状態から始められます。

Factoryでテスト用データを作る

Factoryとは

Factoryは、モデルのテスト用データをまとめて生成するための仕組みです。

毎回手作業でユーザーや投稿のダミーデータを1件ずつ作るのは手間がかかるため、Factoryを使ってルールを1度定義しておきます。

Factoryの定義

<?php
// database/factories/PostFactory.php
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(),
            'body' => $this->faker->paragraph(),
            'user_id' => \App\Models\User::factory(),
            'published' => true,
        ];
    }
}

fakerはダミーデータを自動生成してくれるライブラリで、sentence()paragraph()のようなメソッドで、それっぽい文章を作ってくれます。

user_idUser::factory()を指定することで、投稿に紐づくユーザーも自動的に作成されます。

Factoryを使ってテストデータを作る

定義したFactoryは、テストの中でこう使います。

<?php
use App\Models\Post;

public function test_公開済みの投稿一覧に表示される(): void
{
    Post::factory()->count(3)->create(['published' => true]);

    $response = $this->get('/posts');

    $response->assertSee('全3件');
}

Post::factory()->count(3)->create([...])だけで、公開済みの投稿を3件、DBに保存できます。

1件ずつPost::create([...])と手書きするより、はるかに簡潔です。

Factoryの状態(State)を使い分ける

複数パターンのデータを用意する

「公開済み」と「下書き」のように、状態が異なるデータを用意したい場合は、Factoryに状態を定義しておくと便利です。

<?php
// database/factories/PostFactory.php
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(),
            'body' => $this->faker->paragraph(),
            'user_id' => \App\Models\User::factory(),
            'published' => true,
        ];
    }

    public function draft(): static
    {
        return $this->state(fn (array $attributes) => [
            'published' => false,
        ]);
    }
}
<?php
public function test_下書きの投稿は一覧に表示されない(): void
{
    Post::factory()->draft()->create();

    $response = $this->get('/posts');

    $response->assertDontSee('published_false_title');
}

draft()のような状態メソッドを用意しておくと、「下書きのケースをテストしたい」という意図がテストコードからも読み取りやすくなります。

つまずきやすいポイント:Factoryを使わず手書きでデータを作る

関連データの準備が漏れる問題

Factoryを知る前、私はテスト用のデータをPost::create([...])で毎回手書きしていました。

❌ Before:関連データを手書きし、準備が漏れる

<?php
public function test_投稿一覧に自分の投稿が表示される(): void
{
    $post = Post::create([
        'title' => 'テスト投稿',
        'body' => '本文',
        'published' => true,
        // user_idを指定し忘れている
    ]);

    $response = $this->get('/posts');

    $response->assertSee('テスト投稿');
}

user_idが必須のカラムなのに指定し忘れており、外部キー制約違反でテスト自体がエラー終了してしまいました。

投稿を作るだけなのに「投稿には必ずユーザーが必要」という関連まで毎回意識しなければならず、テストを書くこと自体のハードルが上がってしまいます。

✅ After:Factoryに関連データの生成ルールを任せる

<?php
public function test_投稿一覧に自分の投稿が表示される(): void
{
    $post = Post::factory()->create([
        'title' => 'テスト投稿',
    ]);

    $response = $this->get('/posts');

    $response->assertSee('テスト投稿');
}

Factory側にuser_idの生成ルールを定義しておけば、呼び出し側は「タイトルだけ指定して、あとはFactoryにお任せ」という書き方ができます。

テストコードは「何を検証したいか」に集中して書けるのが理想なので、関連データの準備はできる限りFactoryに任せるようにしています。

Factory・RefreshDatabaseを組み合わせた実践例

一連の流れを1つのテストで確認する

最後に、これまでの内容を組み合わせた実践的なテストを見てみます。

<?php
// tests/Feature/PostControllerTest.php
namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_自分の投稿だけ編集画面を開ける(): void
    {
        $owner = User::factory()->create();
        $otherUser = User::factory()->create();
        $post = Post::factory()->for($owner)->create();

        $response = $this->actingAs($owner)->get("/posts/{$post->id}/edit");
        $response->assertStatus(200);

        $response = $this->actingAs($otherUser)->get("/posts/{$post->id}/edit");
        $response->assertStatus(403);
    }
}

Post::factory()->for($owner)->create()で「特定のユーザーが投稿した記事」を作り、投稿者本人と別ユーザーそれぞれでアクセスした結果を1つのテストで検証しています。

RefreshDatabaseがあるおかげで、このテストは他のテストの影響を受けず、何度実行しても同じ結果になります。

まとめ

この記事のポイント

  • RefreshDatabaseトレイトで、テストごとにDBをまっさらな状態にリセットできる
  • Factoryを使うと、テスト用データの生成ルールを1箇所にまとめられる
  • 状態(State)を定義しておくと、「公開済み」「下書き」のようなパターン違いのデータを簡潔に作れる
  • 関連データの準備をFactoryに任せることで、テストコードは検証したい内容に集中できる

次に読むべき記事

テスト編の基礎はここまでです。

次回は、書いたテストを毎回手動で実行するのではなく、GitHub Actionsを使ってコードをpushするたびに自動実行する仕組みを解説します。

→ 次の記事:GitHub ActionsでLaravelのテストをCI連携する

タイトルとURLをコピーしました