なぜテストを書くか——個人・少人数開発でこそ重要
「テストを書く時間がない」という声はよく聞きます。しかし特に少人数チームや個人開発では、テストがなければ機能追加のたびに手動確認が必要になり、やがてリファクタリングが怖くてできなくなります。
テストがある状態のメリット:
- 変更後の動作確認が自動化される
- バグを早期に発見できる
- コードを安心してリファクタリングできる
- 仕様の文書代わりになる
本記事ではLaravelのPHPUnit + Feature Testの実践的な書き方を解説します。
テストの種類
| 種類 | 対象 | 特徴 |
|---|---|---|
| Unit Test | クラス・メソッド単体 | 高速・DBなし |
| Feature Test | HTTPリクエスト〜レスポンス | 実際の動作に近い |
| Browser Test | ブラウザ操作 | Dusk使用・低速 |
まずFeature Testから始めることをお勧めします。実際のリクエストを模擬するため、バグを見つけやすいです。
最初のFeatureテスト
php artisan make:test ContactControllerTest
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ContactControllerTest extends TestCase
{
use RefreshDatabase;
public function test_お問い合わせフォームが正常に表示される(): void
{
$response = $this->get('/contact');
$response->assertStatus(200);
$response->assertViewIs('contact.index');
$response->assertSee('お問い合わせ');
}
public function test_有効なデータを送信するとDBに保存される(): void
{
$response = $this->post('/contact', [
'name' => '山田太郎',
'email' => 'yamada@example.com',
'subject' => 'システム開発のご相談',
'body' => 'Webシステムの開発についてご相談したいです。',
'agreed' => '1',
]);
$response->assertRedirect('/contact/complete');
$this->assertDatabaseHas('contacts', [
'email' => 'yamada@example.com',
'subject' => 'システム開発のご相談',
]);
}
public function test_メールアドレスが不正な場合はバリデーションエラー(): void
{
$response = $this->post('/contact', [
'name' => '山田太郎',
'email' => 'not-an-email',
'body' => 'テスト',
]);
$response->assertSessionHasErrors('email');
$response->assertRedirect();
}
}
Factory——テストデータの生成
php artisan make:factory OrderFactory --model=Order
<?php
namespace Database\Factories;
use App\Models\Customer;
use Illuminate\Database\Eloquent\Factories\Factory;
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => 'ORD-' . $this->faker->unique()->numerify('######'),
'customer_id' => Customer::factory(),
'total' => $this->faker->randomFloat(2, 1000, 50000),
'status' => $this->faker->randomElement(['pending', 'confirmed', 'shipped', 'delivered']),
'note' => $this->faker->optional()->sentence(),
'ordered_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
];
}
// ステートで特定の状態のモデルを作りやすくする
public function pending(): static
{
return $this->state(['status' => 'pending']);
}
public function delivered(): static
{
return $this->state([
'status' => 'delivered',
'delivered_at' => now(),
]);
}
}
認証が必要なエンドポイントのテスト
class OrderControllerTest extends TestCase
{
use RefreshDatabase;
public function test_未認証ユーザーは注文一覧にアクセスできない(): void
{
$this->get('/admin/orders')->assertRedirect('/login');
}
public function test_管理者は注文一覧を閲覧できる(): void
{
$admin = User::factory()->create(['role' => 'admin']);
// actingAsで認証ユーザーとして実行
$response = $this->actingAs($admin)->get('/admin/orders');
$response->assertStatus(200);
$response->assertViewIs('admin.orders.index');
}
public function test_注文を確定するとステータスが変わる(): void
{
$admin = User::factory()->create(['role' => 'admin']);
$order = Order::factory()->pending()->create();
$response = $this->actingAs($admin)
->patch("/admin/orders/{$order->id}/confirm");
$response->assertRedirect();
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'status' => 'confirmed',
]);
}
}
メール送信のテスト
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmation;
public function test_注文確定時に確認メールが送信される(): void
{
Mail::fake();
$order = Order::factory()->pending()->create();
$this->actingAs($admin)->patch("/admin/orders/{$order->id}/confirm");
Mail::assertQueued(OrderConfirmation::class, function ($mail) use ($order) {
return $mail->hasTo($order->customer->email);
});
}
テストのグループ化と実行
# 全テスト実行
php artisan test
# 特定のテストクラスのみ
php artisan test --filter=ContactControllerTest
# 並列実行(高速化)
php artisan test --parallel
# カバレッジレポート生成
php artisan test --coverage
データプロバイダー——複数ケースをまとめてテスト
/**
* @dataProvider validationProvider
*/
public function test_バリデーション(array $input, string $errorField): void
{
$this->post('/contact', $input)
->assertSessionHasErrors($errorField);
}
public static function validationProvider(): array
{
return [
'名前が空' => [['name' => '', 'email' => 'a@b.com', 'body' => 'test', 'agreed' => '1'], 'name'],
'メールが不正' => [['name' => '山田', 'email' => 'invalid', 'body' => 'test', 'agreed' => '1'], 'email'],
'本文が短すぎ' => [['name' => '山田', 'email' => 'a@b.com', 'body' => 'short', 'agreed' => '1'], 'body'],
];
}
まとめ
LaravelのFeatureテストはHTTPリクエストから始まり、DB・メール・イベントまでを網羅的にテストできます。まず主要なユーザーフロー(フォーム送信・ログイン・CRUD)のテストを書くことから始めてください。弊社では全プロジェクトでFeatureテストを整備し、デプロイ前にCIで自動実行しています。
LaravelによるWebシステム開発のご相談はお気軽にどうぞ。