株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Laravelでテストを書く習慣——PHPUnit + Featuresテストの基本
ブログ一覧へ
技術ブログ

Laravelでテストを書く習慣——PHPUnit + Featuresテストの基本

テストを書くことでリグレッションを防ぎ、自信を持ってリリースできます。Laravelのテスト機能を使った実用的なFeatureテストの書き方を解説します。

なぜテストを書くか——個人・少人数開発でこそ重要

「テストを書く時間がない」という声はよく聞きます。しかし特に少人数チームや個人開発では、テストがなければ機能追加のたびに手動確認が必要になり、やがてリファクタリングが怖くてできなくなります。

テストがある状態のメリット:

  • 変更後の動作確認が自動化される
  • バグを早期に発見できる
  • コードを安心してリファクタリングできる
  • 仕様の文書代わりになる

本記事では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システム開発のご相談はお気軽にどうぞ。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →