株式会社WR

株式会社WR

WEB TOTAL CONSULTING

PestでLaravelのテストをモダンに書く
ブログ一覧へ
技術ブログ

PestでLaravelのテストをモダンに書く

Pestはphpunit上で動くモダンなPHPテストフレームワークです。流れるようなAPIとアーキテクチャ的なテストで、Laravelのテストコードを読みやすく書けます。

PestとはLaravelのモダンなテストフレームワーク

PestはPHPUnitの上に構築されたテストフレームワークで、より少ない記述量でわかりやすいテストが書けるように設計されています。Laravel 11のスターターキットではPestがデフォルトになっています。


PHPUnitとPestの比較

// PHPUnit:クラスベース
class UserTest extends TestCase
{
    public function test_ユーザーは管理者かどうか確認できる(): void
    {
        $user = User::factory()->create(['role' => 'admin']);
        $this->assertTrue($user->isAdmin());
    }
}

// Pest:関数ベース(より少ない記述量)
test('ユーザーは管理者かどうか確認できる', function () {
    $user = User::factory()->create(['role' => 'admin']);
    expect($user->isAdmin())->toBeTrue();
});

Pestのインストール

composer require pestphp/pest --dev --with-all-dependencies
php artisan pest:install

# プラグイン
composer require pestphp/pest-plugin-laravel --dev

テストの書き方

基本的なアサーション

// expect() チェーン
test('商品の価格は正の整数である', function () {
    $product = Product::factory()->create(['price' => 5000]);

    expect($product->price)
        ->toBeInt()
        ->toBeGreaterThan(0)
        ->toBeLessThanOrEqual(999999);
});

// 複数の値をまとめてアサート
test('ユーザー情報が正しく登録される', function () {
    $user = User::factory()->create([
        'name'  => '山田太郎',
        'email' => 'yamada@example.com',
    ]);

    expect($user)
        ->name->toBe('山田太郎')
        ->email->toBe('yamada@example.com')
        ->email_verified_at->toBeNull();
});

HTTPテスト

// Laravelのテストヘルパーとの組み合わせ
test('お問い合わせフォームが正常に送信される', function () {
    $response = $this->post('/contact', [
        'name'    => '山田太郎',
        'email'   => 'yamada@example.com',
        'subject' => 'テスト',
        'body'    => 'テストメッセージです。',
        'agreed'  => '1',
    ]);

    $response
        ->assertStatus(302)
        ->assertRedirect('/contact/complete');

    expect(Contact::count())->toBe(1);
});

test('未認証ユーザーはダッシュボードにアクセスできない', function () {
    $this->get('/dashboard')->assertRedirect('/login');
});

test('認証済みユーザーはダッシュボードにアクセスできる', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
         ->get('/dashboard')
         ->assertOk()
         ->assertViewIs('dashboard');
});

describe でテストをグループ化

describe('注文の管理', function () {

    beforeEach(function () {
        $this->admin = User::factory()->admin()->create();
        $this->order = Order::factory()->pending()->create();
    });

    test('注文一覧を取得できる', function () {
        $this->actingAs($this->admin)
             ->get('/admin/orders')
             ->assertOk()
             ->assertSee($this->order->order_number);
    });

    test('注文を確定できる', function () {
        $this->actingAs($this->admin)
             ->patch("/admin/orders/{$this->order->id}/confirm")
             ->assertRedirect();

        expect($this->order->fresh()->status)->toBe('confirmed');
    });

    test('既に確定済みの注文は再確定できない', function () {
        $confirmed = Order::factory()->confirmed()->create();

        $this->actingAs($this->admin)
             ->patch("/admin/orders/{$confirmed->id}/confirm")
             ->assertStatus(422);
    });
});

データセット(Data-Driven Testing)

// 複数のデータでテストを繰り返す
dataset('無効なメールアドレス', [
    ['invalid'],
    ['@example.com'],
    ['user@'],
    ['user @example.com'],
    [''],
]);

test('無効なメールアドレスはバリデーションエラーになる', function (string $email) {
    $this->post('/contact', ['email' => $email])
         ->assertSessionHasErrors('email');
})->with('無効なメールアドレス');

モック・スパイ

test('注文確定時にメールが送信される', function () {
    Mail::fake();

    $order = Order::factory()->pending()->create();
    $admin = User::factory()->admin()->create();

    $this->actingAs($admin)
         ->patch("/admin/orders/{$order->id}/confirm");

    Mail::assertQueued(OrderConfirmation::class, function ($mail) use ($order) {
        return $mail->hasTo($order->customer_email);
    });
});

test('外部APIが失敗した場合のエラーハンドリング', function () {
    Http::fake([
        'api.external.com/*' => Http::response(['error' => 'Service Unavailable'], 503),
    ]);

    expect(fn() => app(ExternalApiService::class)->fetch())
        ->toThrow(\RuntimeException::class, 'API request failed');
});

テストの実行

# 全テスト実行
./vendor/bin/pest

# 並列実行(高速)
./vendor/bin/pest --parallel

# 特定のテストのみ
./vendor/bin/pest --filter="注文の管理"

# カバレッジ表示
./vendor/bin/pest --coverage --min=80

# Laravel Artisanから
php artisan test

まとめ

Pestを使うとPHPUnitより少ない記述量で読みやすいテストが書けます。describebeforeEachdataset を組み合わせることで、複雑なテストシナリオも整理された形で記述できます。弊社では新規プロジェクトからPestを採用しています。

LaravelによるWebシステム開発のご相談はお気軽にどうぞ。

Category 技術ブログ

Related Posts

関連記事

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

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

お問い合わせ →