LaravelでPHPUnitを用いた機能テスト
概要
最近Laravelの機能テストを実装する機会があり、基本的な書き方をまとめてみました。導入する際は少し手間がかかりますが、それ以上にメリットが大きいなと感じました。
テストを書くメリット
機能テストを導入することによって得られるメリットをいくつか挙げてみます。
リファクタリングの安心感
既存コードを変更する際に、テストが通ることで「機能が壊れていない」ことを確認できます。特にControllerやMiddlewareの変更時には心理的な安心感が大きいです。
仕様の明文化
テストコード自体が仕様書の役割を果たします。「このAPIはどういう動作をするのか」「認証が必要なのか」といった情報がコードから読み取れます。
デグレード防止
新機能追加時に既存機能が壊れることを防げます。継続的インテグレーション(CI)と組み合わせることで、マージ前に問題を検出できます。
デバッグ効率の向上
バグが発生した際、まずテストを書いてバグを再現し、その後修正するという流れで進めることで、修正の確実性が高まります。
テストを書く上での基本的な考え方
AAA(Arrange-Act-Assert)パターン
テストの構造を明確にするため、以下の3段階で整理しています:
- Arrange(準備): テストに必要なデータや状態を準備
- Act(実行): テスト対象の処理を実行
- Assert(検証): 期待する結果と実際の結果を比較
境界値を意識する
正常系だけでなく異常系もテストします。例えば認証が必要な機能では、認証済み・未認証の両パターンを確認することが重要です。
独立性を保つ
各テストは他のテストの結果に依存しないように作成しています。RefreshDatabase
トレイトを使うことで、テスト間でのデータの干渉を防いでいます。
意味のあるテスト名
テスト名を見ただけで「何をテストしているか」が分かるように命名しています。test_user_can_create_post
のように、主語・動詞・目的語を明確にしています。
環境設定
テスト用データベース設定
.env.testing
を作成してテスト専用のデータベース設定を分離しています。メモリ上のSQLiteを使うことでテスト実行速度を向上させることができます。
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
基本的なテストの書き方
GETリクエストのテスト
<?php
namespace Tests\Feature;
use Tests\TestCase;
class BasicTest extends TestCase
{
public function test_top_page_returns_successful_response()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
シンプルですが、ページが正常に表示されることを確認できます。
認証機能のテスト
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthTest extends TestCase
{
use RefreshDatabase;
public function test_guest_redirected_to_login()
{
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
}
public function test_authenticated_user_can_access_dashboard()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertStatus(200);
}
}
actingAs()
メソッドを使うことで認証済み状態を簡単にシミュレートできます。RefreshDatabase
トレイトでテスト間のデータベース状態をクリーンに保つことができます。
CRUD操作のテスト
記事投稿機能を例にCRUD操作をテストしてみます。
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_create_post()
{
$user = User::factory()->create();
$postData = [
'title' => 'テスト記事',
'content' => 'テスト内容'
];
$response = $this->actingAs($user)->post('/posts', $postData);
$response->assertRedirect();
$this->assertDatabaseHas('posts', [
'title' => 'テスト記事',
'user_id' => $user->id
]);
}
public function test_user_can_update_own_post()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$updateData = [
'title' => '更新後タイトル',
'content' => '更新後内容'
];
$response = $this->actingAs($user)->put("/posts/{$post->id}", $updateData);
$response->assertRedirect();
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'title' => '更新後タイトル'
]);
}
public function test_user_can_delete_own_post()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)->delete("/posts/{$post->id}");
$response->assertRedirect();
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
}
}
バリデーションテスト
フォームバリデーションが正常に動作するかも確認しています。
public function test_post_creation_validates_required_fields()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'content' => '内容のみ'
]);
$response->assertSessionHasErrors('title');
$this->assertDatabaseCount('posts', 0);
}
API機能のテスト
JSON APIのテストも同様に書けます。
public function test_api_returns_posts_collection()
{
$user = User::factory()->create();
Post::factory()->count(3)->create();
$response = $this->actingAs($user)->get('/api/posts');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => ['id', 'title', 'content', 'created_at']
]
]);
}
便利なアサーションメソッド
// レスポンス関連
$response->assertStatus(200);
$response->assertOk();
$response->assertRedirect('/path');
// データベース関連
$this->assertDatabaseHas('table', ['column' => 'value']);
$this->assertDatabaseMissing('table', ['column' => 'value']);
$this->assertDatabaseCount('table', 5);
// セッション関連
$response->assertSessionHas('key');
$response->assertSessionHasErrors('field');
// JSON関連
$response->assertJson(['key' => 'value']);
$response->assertJsonStructure(['data' => ['*' => ['id', 'name']]]);
実行方法
# 全テスト実行
php artisan test
# 特定ファイルのみ実行
php artisan test tests/Feature/PostTest.php
# 特定メソッドのみ実行
php artisan test --filter test_user_can_create_post
# カバレッジ表示(要Xdebug)
php artisan test --coverage
まとめ
機能テストを書くことで:
- リファクタリング時の安心感が得られる
- 仕様の変更による影響範囲を把握しやすくなる
- デプロイ前の動作確認を自動化できる
というメリットがあります。
最初はかなり面倒に感じるかもしれませんが、長期的に見ると開発速度は上がると思います。まずは主要な機能から書いていき、新しい機能を実装する際はテストも一緒に書いていくのが良さそうですね。
導入を検討している方の参考になれば幸いです。