Laravelのテスト戦略(PHPUnit & Pest)

モダンなWebアプリケーション開発において、テストは品質保証の要となります。特にLaravelのようなフレームワークでは、堅牢なテスト機能が標準で組み込まれており、効率的なテスト駆動開発(TDD)を実践できます。この記事では、LaravelにおけるPHPUnitとPestを使用したテスト戦略について詳しく解説します。

目次

  1. Laravelのテスト環境
  2. PHPUnitによるテスト
  3. Pestによるテスト
  4. テストの種類と使い分け
  5. テストの書き方のベストプラクティス
  6. CI/CDパイプラインとの統合
  7. テストカバレッジの向上
  8. まとめ

Laravelのテスト環境

Laravelは、新しいプロジェクトを作成した時点で、すぐに使えるテスト環境が整っています。デフォルトでは、testsディレクトリにFeatureUnitの2つのディレクトリがあり、それぞれ異なるタイプのテストを格納します。

テスト環境のセットアップ

新規Laravelプロジェクトでは、PHPUnitが標準でインストールされていますが、phpunit.xmlファイルで設定をカスタマイズできます。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

特に重要なのは、テスト環境用の設定として、インメモリのSQLiteデータベースを使用し、キャッシュやセッションにはarrayドライバを使用することで、テストの実行速度を向上させています。

Pestのインストール

最近人気が高まっているPest(PHPUnitの上に構築された表現力豊かなテストフレームワーク)を使用する場合は、以下のコマンドでインストールします:

composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install

これにより、Pestの設定ファイルが生成され、テストの実行環境が整います。

PHPUnitによるテスト

PHPUnitは、PHPでの標準的なテスティングフレームワークであり、Laravelでもデフォルトで使用されています。

基本的なユニットテスト

tests/Unitディレクトリには、アプリケーションの個々の部分(クラスやメソッド)をテストするためのユニットテストを配置します。

<?php

namespace Tests\Unit;

use App\Models\User;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_user_has_full_name_attribute()
    {
        $user = new User([
            'first_name' => '太郎',
            'last_name' => '山田',
        ]);

        $this->assertEquals('山田 太郎', $user->full_name);
    }
}

フィーチャーテスト

tests/Featureディレクトリには、アプリケーションの機能全体をテストするためのフィーチャーテストを配置します。これらのテストでは、HTTPリクエストを模倣してレスポンスをテストできます。

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_view_profile()
    {
        // ユーザーを作成
        $user = User::factory()->create();

        // 認証してプロフィールページにアクセス
        $response = $this->actingAs($user)
                         ->get("/users/{$user->id}");

        $response->assertStatus(200)
                 ->assertSee($user->name);
    }

    public function test_user_can_update_profile()
    {
        $user = User::factory()->create();
        $newData = ['name' => '新しい名前'];

        $response = $this->actingAs($user)
                         ->put("/users/{$user->id}", $newData);

        $response->assertRedirect("/users/{$user->id}");
        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'name' => '新しい名前',
        ]);
    }
}

データベーステスト

Laravelは、テスト中にデータベースをリフレッシュするための便利なトレイトを提供しています:

use Illuminate\Foundation\Testing\RefreshDatabase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    // テストメソッド...
}

RefreshDatabaseトレイトを使用すると、各テストの前にマイグレーションが実行され、テスト間でのデータの干渉を防ぎます。

モックとスタブ

外部サービスやリソースに依存するクラスをテストする場合、モックやスタブが非常に役立ちます:

public function test_newsletter_can_be_sent()
{
    // メールサービスをモック
    Mail::fake();

    // ニュースレター送信のテスト
    $newsletter = new Newsletter();
    $newsletter->send();

    // メールが送信されたことを検証
    Mail::assertSent(NewsletterMail::class, function ($mail) {
        return $mail->hasTo('subscribers@example.com');
    });
}

Pestによるテスト

Pestは、PHPUnitの上に構築された表現力豊かなテストフレームワークで、より簡潔で読みやすいテストを書くことができます。

基本的なPestテスト

PHPUnitの例を、Pestで書き直すとこのようになります:

<?php

use App\Models\User;

test('user has full name attribute', function () {
    $user = new User([
        'first_name' => '太郎',
        'last_name' => '山田',
    ]);

    expect($user->full_name)->toBe('山田 太郎');
});

フィーチャーテスト(Pest版)

<?php

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('user can view profile', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)
                     ->get("/users/{$user->id}");

    $response->assertStatus(200)
             ->assertSee($user->name);
});

test('user can update profile', function () {
    $user = User::factory()->create();
    $newData = ['name' => '新しい名前'];

    $response = $this->actingAs($user)
                     ->put("/users/{$user->id}", $newData);

    $response->assertRedirect("/users/{$user->id}");
    $this->assertDatabaseHas('users', [
        'id' => $user->id,
        'name' => '新しい名前',
    ]);
});

Pestのメリット

  1. 簡潔な構文: テストの記述が簡潔で読みやすい
  2. Expectationsのチェーン: 期待値のチェックをチェーンできる
  3. Higher Order Testing: 複数のデータセットに対して同じテストを実行しやすい
  4. テストのグループ化: 関連するテストを論理的にグループ化できる
// Higher Order Testing の例
it('calculates the correct price with tax', function ($price, $taxRate, $expected) {
    $calculator = new PriceCalculator();
    expect($calculator->calculateWithTax($price, $taxRate))->toBe($expected);
})->with([
    [100, 0.1, 110],
    [200, 0.1, 220],
    [100, 0.2, 120],
]);

// テストのグループ化の例
describe('User', function () {
    it('can be created', function () {
        // ...
    });

    it('can be updated', function () {
        // ...
    });

    it('can be deleted', function () {
        // ...
    });
});

テストの種類と使い分け

Laravelアプリケーションをテストする際に、いくつかの異なるタイプのテストを使い分けると効果的です:

1. ユニットテスト

個々のクラスやメソッドの機能をテストします。外部依存関係はモックやスタブに置き換えて、コードの単一の「ユニット」のみをテストします。

適した場合:

  • バリデーションルールのテスト
  • ヘルパー関数のテスト
  • 複雑なロジックを持つサービスクラスのテスト
// PHPUnit
public function test_tax_calculator_calculates_correctly()
{
    $calculator = new TaxCalculator();
    $this->assertEquals(110, $calculator->calculate(100, 0.1));
}

// Pest
test('tax calculator calculates correctly', function () {
    $calculator = new TaxCalculator();
    expect($calculator->calculate(100, 0.1))->toBe(110);
});

2. フィーチャーテスト

ユーザーの視点からアプリケーションの機能全体をテストします。HTTPリクエストを模倣し、レスポンスを検証します。

適した場合:

  • コントローラのテスト
  • ミドルウェアのテスト
  • API エンドポイントのテスト
// PHPUnit
public function test_user_can_create_post()
{
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)
                     ->post('/posts', [
                         'title' => 'テスト投稿',
                         'content' => 'これはテスト投稿です。'
                     ]);
    
    $response->assertRedirect('/posts');
    $this->assertDatabaseHas('posts', [
        'title' => 'テスト投稿',
        'user_id' => $user->id
    ]);
}

// Pest
test('user can create post', function () {
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)
                     ->post('/posts', [
                         'title' => 'テスト投稿',
                         'content' => 'これはテスト投稿です。'
                     ]);
    
    $response->assertRedirect('/posts');
    expect(Post::where('title', 'テスト投稿')
               ->where('user_id', $user->id)
               ->exists())->toBeTrue();
});

3. インテグレーションテスト

複数のコンポーネントが連携して動作することをテストします。例えば、サービスクラスとデータベースのやり取りなどです。

適した場合:

  • リポジトリパターンをテスト
  • サービスクラスとデータベースの連携
  • 複数のサービスの統合
// PHPUnit
public function test_post_service_creates_and_notifies()
{
    $user = User::factory()->create();
    Mail::fake();
    
    $service = new PostService();
    $post = $service->createPost($user, [
        'title' => 'テスト投稿',
        'content' => 'これはテスト投稿です。'
    ]);
    
    $this->assertInstanceOf(Post::class, $post);
    $this->assertEquals('テスト投稿', $post->title);
    Mail::assertSent(PostCreatedNotification::class);
}

// Pest
test('post service creates and notifies', function () {
    $user = User::factory()->create();
    Mail::fake();
    
    $service = new PostService();
    $post = $service->createPost($user, [
        'title' => 'テスト投稿',
        'content' => 'これはテスト投稿です。'
    ]);
    
    expect($post)->toBeInstanceOf(Post::class);
    expect($post->title)->toBe('テスト投稿');
    Mail::assertSent(PostCreatedNotification::class);
});

4. ブラウザテスト(Laravel Dusk)

実際のブラウザでJavaScriptの動作も含めてテストします。

// DuskTest
public function test_user_can_login()
{
    $user = User::factory()->create([
        'email' => 'test@example.com',
        'password' => bcrypt('password'),
    ]);
    
    $this->browse(function ($browser) {
        $browser->visit('/login')
                ->type('email', 'test@example.com')
                ->type('password', 'password')
                ->press('Login')
                ->assertPathIs('/dashboard');
    });
}

テストの書き方のベストプラクティス

1. テスト名は明確に

テスト名からそのテストが何をテストしているのかが分かるようにします。

// 悪い例
public function test_create()
// 良い例
public function test_user_can_create_post_when_authenticated()

// Pestの場合
test('user can create post when authenticated', function () {
    // ...
});

2. AAA (Arrange-Act-Assert) パターンの使用

テストは3つの部分に分けて構成すると理解しやすくなります:

  • Arrange: テストに必要なセットアップ
  • Act: テスト対象のメソッドやアクションの実行
  • Assert: 結果の検証
public function test_post_can_be_created()
{
    // Arrange
    $user = User::factory()->create();
    $this->actingAs($user);
    $postData = ['title' => 'テスト投稿', 'content' => 'これはテスト投稿です。'];
    
    // Act
    $response = $this->post('/posts', $postData);
    
    // Assert
    $response->assertRedirect('/posts');
    $this->assertDatabaseHas('posts', [
        'title' => 'テスト投稿',
        'user_id' => $user->id
    ]);
}

3. テストデータの作成にファクトリーを使用

テストデータの作成には、モデルファクトリーを使用すると便利です:

// UserFactory.php
public function definition()
{
    return [
        'name' => $this->faker->name(),
        'email' => $this->faker->unique()->safeEmail(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
    ];
}

// テスト内での使用
$user = User::factory()->create();
$admin = User::factory()->state(['role' => 'admin'])->create();
$users = User::factory()->count(3)->create();

4. テストのグループ化とデータプロバイダの活用

同様のテストを複数のデータセットで実行するには、データプロバイダを活用します:

// PHPUnit
/**
 * @dataProvider validationDataProvider
 */
public function test_validation_rules($data, $field, $expectedError)
{
    $response = $this->post('/posts', $data);
    $response->assertSessionHasErrors([$field => $expectedError]);
}

public function validationDataProvider()
{
    return [
        [['title' => '', 'content' => 'コンテンツ'], 'title', 'The title field is required.'],
        [['title' => 'あ', 'content' => 'コンテンツ'], 'title', 'The title must be at least 3 characters.'],
        [['title' => 'タイトル', 'content' => ''], 'content', 'The content field is required.'],
    ];
}

// Pest
it('validates post fields', function ($data, $field, $expectedError) {
    $response = $this->post('/posts', $data);
    $response->assertSessionHasErrors([$field => $expectedError]);
})->with([
    [['title' => '', 'content' => 'コンテンツ'], 'title', 'The title field is required.'],
    [['title' => 'あ', 'content' => 'コンテンツ'], 'title', 'The title must be at least 3 characters.'],
    [['title' => 'タイトル', 'content' => ''], 'content', 'The content field is required.'],
]);

5. テストヘルパーとカスタムアサーションの作成

繰り返し使用するテストのセットアップロジックは、ヘルパーメソッドやカスタムアサーションとして抽出します:

// TestCase.phpに追加
protected function loginAsAdmin()
{
    $admin = User::factory()->state(['role' => 'admin'])->create();
    $this->actingAs($admin);
    return $admin;
}

// カスタムアサーションの追加
public function assertModelExists($model)
{
    $this->assertTrue($model->exists, 'モデルがデータベースに存在しません。');
    return $this;
}

6. テスト環境の分離

テストが本番環境やデータに影響を与えないよう、テスト環境を分離します:

  • テスト用の.env.testingファイルを作成
  • インメモリデータベースを使用
  • キューやメールをフェイクに置き換え

CI/CDパイプラインとの統合

テストを継続的インテグレーション/継続的デプロイメント(CI/CD)パイプラインに統合することで、コードの品質を常に高く保つことができます。

GitHub Actionsでの設定例

name: Laravel Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v2
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, dom, fileinfo, mysql
        coverage: xdebug
    
    - name: Copy .env
      run: cp .env.example .env.testing
    
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    
    - name: Generate key
      run: php artisan key:generate --env=testing
    
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    
    - name: Run PHPUnit Tests
      env:
        DB_CONNECTION: mysql
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: laravel_test
        DB_USERNAME: root
        DB_PASSWORD: password
      run: vendor/bin/phpunit --coverage-clover=coverage.xml
    
    # Pestを使用する場合
    - name: Run Pest Tests
      env:
        DB_CONNECTION: mysql
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: laravel_test
        DB_USERNAME: root
        DB_PASSWORD: password
      run: vendor/bin/pest --coverage-clover=pest-coverage.xml
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v1
      with:
        file: ./coverage.xml

テストカバレッジの向上

テストカバレッジとは、コードベースのどれだけの部分がテストでカバーされているかを示す指標です。

カバレッジレポートの生成

PHPUnitでカバレッジレポートを生成するには:

# HTMLレポートを生成
php artisan test --coverage --coverage-html=coverage

# Pestでカバレッジレポートを生成
./vendor/bin/pest --coverage --coverage-html=coverage

効果的なカバレッジ向上の戦略

  1. 複雑なロジックを優先: 条件分岐が多いメソッドを優先的にテストする
  2. エッジケースを考慮: 境界値や例外ケースも含めてテストする
  3. リファクタリングの際に追加: コードをリファクタリングする際には必ずテストを追加する
  4. バグ修正時にテスト追加: バグを修正した際には、そのバグを検出するためのテストを追加する

まとめ

LaravelでのPHPUnitとPestを活用したテスト戦略についてまとめました。それぞれのフレームワークには特徴があり、プロジェクトの要件や開発チームの好みに応じて選択できます。

PHPUnitの利点

  • Laravelに標準搭載
  • 多くの開発者に馴染みがある
  • 豊富なアサーションとヘルパー

Pestの利点

  • 直感的で読みやすい構文
  • より少ないコードでテストを書ける
  • Higher Order Testing機能

効果的なテスト戦略を実装するためのポイントを押さえておきましょう:

  1. テストの種類を適切に使い分ける
  2. テストコードを明確で読みやすく保つ
  3. 継続的インテグレーションに統合する
  4. テストカバレッジを定期的に確認する
  5. テストを書く文化をチーム内で育てる

テストはコードの品質を保証するだけでなく、ドキュメントとしての役割も果たします。新しいチームメンバーがコードベースを理解するのに役立ち、リファクタリングや機能追加の際の安全網にもなります。

質の高いテストを書き、継続的に実行することで、Laravel アプリケーションの信頼性と保守性を高めましょう。

コメント

タイトルとURLをコピーしました