こんにちは、かつコーチです。
前回は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_idにUser::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連携する