PHPUnit完全ガイド:実践で使える7つのテスト手法と設定のベストプラクティス【2025年版】

目次

目次へ

PHPUnit とは:Web アプリケーションの品質を支えるテストフレームワーク

PHPUnitは、PHP言語用の単体テストフレームワークとして最も広く使用されているツールです。Sebastian Bergmannによって開発され、xUnitアーキテクチャに基づいて設計されています。多くの主要なPHPプロジェクトやフレームワーク(Laravel、Symfony、WordPressなど)で採用されており、信頼性の高いテスト環境を提供します。

PHPUnit が解決する3つの開発現場の課題

  1. 品質保証の効率化と自動化
  • 手動テストによる人的ミスの削減
  • 継続的なリグレッションテストの実現
  • テスト実行時間の大幅な短縮
   // 従来の手動テスト
   $result = someFunction();
   if ($result === expected) {
       echo "Test passed";
   }

   // PHPUnitを使用した自動化テスト
   public function testSomeFunction(): void
   {
       $this->assertEquals(expected, someFunction());
   }
  1. 開発速度と品質の両立
  • テストファーストな開発アプローチの実現
  • 早期のバグ発見による修正コストの削減
  • リファクタリングの安全性確保
   // テストファーストな開発例
   public function testCalculateTotal(): void
   {
       $cart = new ShoppingCart();
       $cart->addItem(new Product("A", 100), 2);
       $cart->addItem(new Product("B", 150), 1);

       $this->assertEquals(350, $cart->calculateTotal());
   }
  1. チーム開発におけるコード品質の標準化
  • 統一されたテスト基準の確立
  • コードレビューの効率化
  • 新規メンバーの学習曲線の緩和

PHPUnitの主要機能と特徴

1. 豊富なアサーションメソッド

PHPUnitは、様々な検証シナリオに対応する多様なアサーションメソッドを提供します:

アサーションタイプ主な用途使用例
assertEquals()値の一致確認$this->assertEquals($expected, $actual);
assertInstanceOf()オブジェクトの型確認$this->assertInstanceOf(ExpectedClass::class, $object);
assertContains()配列・文字列の包含確認$this->assertContains($needle, $haystack);
assertNull()null値の確認$this->assertNull($value);

2. モックとスタブ機能

外部依存を持つコードのテストを容易にする強力なモック機能を提供:

// データベース接続をモック化する例
$mock = $this->createMock(Database::class);
$mock->method('query')
     ->willReturn(['id' => 1, 'name' => 'Test']);

3. データプロバイダー機能

複数のテストケースを効率的に実行できる機能を提供:

/**
 * @dataProvider additionProvider
 */
public function testAdd($a, $b, $expected): void
{
    $this->assertEquals($expected, add($a, $b));
}

public function additionProvider(): array
{
    return [
        [1, 1, 2],
        [0, 1, 1],
        [-1, 1, 0]
    ];
}

4. カバレッジレポート機能

テストカバレッジを視覚的に確認できる機能を提供し、テスト品質の定量的な評価を可能にします。

PHPUnit環境構築の完全マニュアル

Composerを使用した最新版のインストール方法

PHPUnitを開発プロジェクトに導入する最も推奨される方法は、Composerを使用することです。以下に、段階的なインストール手順を示します。

  1. 事前準備
   # プロジェクトディレクトリの作成と移動
   mkdir my-project
   cd my-project

   # Composerプロジェクトの初期化
   composer init --require="phpunit/phpunit:^10.0" --dev
  1. PHPUnitのインストール
   # PHPUnitをdev依存関係としてインストール
   composer require --dev phpunit/phpunit ^10.0

   # インストールの確認
   ./vendor/bin/phpunit --version
  1. プロジェクト構造のセットアップ
   my-project/
   ├── src/           # ソースコード
   ├── tests/         # テストコード
   ├── composer.json
   ├── composer.lock
   └── phpunit.xml    # PHPUnit設定ファイル

設定ファイルphpunit.xmlの作成と重要な設定項目

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"
         testdox="true"
         displayDetailsOnTestsThatTriggerWarnings="true"
         displayDetailsOnTestsThatTriggerNotices="true"
         displayDetailsOnTestsThatTriggerErrors="true"
         displayDetailsOnTestsThatTriggerDeprecations="true">

    <!-- テストスイートの設定 -->
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>

    <!-- コードカバレッジレポートの設定 -->
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <report>
            <html outputDirectory="coverage"/>
            <text outputFile="coverage.txt"/>
        </report>
    </coverage>

    <!-- PHP設定 -->
    <php>
        <env name="APP_ENV" value="testing"/>
        <ini name="error_reporting" value="-1"/>
        <ini name="memory_limit" value="512M"/>
    </php>
</phpunit>

主要な設定項目の解説:

設定項目説明推奨値
bootstrapオートローダーの指定vendor/autoload.php
colorsテスト結果の色付け表示true
testdox読みやすいテスト結果表示true
memory_limitテスト実行時のメモリ制限512M以上

IDEとの連携でテストを効率化する方法

主要なIDEでのPHPUnit設定と効率的なテスト実行方法を解説します。

1. PhpStorm での設定

  1. テスト実行環境の設定
   Settings > Languages & Frameworks > PHP > Test Frameworks
  • PHPUnitのパスをvendor/autoload.phpに設定
  • テスト設定ファイル(phpunit.xml)のパスを指定
  1. ショートカットキーの活用
  • Ctrl + Shift + T:テストクラスの作成
  • Ctrl + Shift + F10:カーソル位置のテスト実行
  • Alt + Shift + F10:テスト実行設定の選択

2. Visual Studio Code での設定

  1. 推奨拡張機能のインストール
  • PHP Extension Pack
  • PHPUnit Test Explorer
  1. settings.jsonの設定例
   {
       "php.validate.enable": true,
       "phpunit.phpunit": "./vendor/bin/phpunit",
       "phpunit.args": [
           "--configuration",
           "./phpunit.xml"
       ]
   }

3. テスト実行の効率化テクニック

  1. ファイルウォッチャーの設定
   # Laravel Mix使用時の例
   mix.browserSync('localhost:8000')
      .version()
      .watch(['tests/**/*.php'], () => {
          exec('./vendor/bin/phpunit');
      });
  1. テストフィルタリング
   # 特定のテストグループのみ実行
   ./vendor/bin/phpunit --group=feature

   # 特定のテストクラスのみ実行
   ./vendor/bin/phpunit tests/Unit/UserTest.php

これらの設定により、効率的なテスト駆動開発(TDD)のワークフローを実現できます。

実践で使える7つのPHPUnitテスト手法

基本的なアサーションを使用したテストケース作成

基本的なアサーションを使用したテストは、PHPUnitの基礎となります。以下に、実践的な例を示します:

class CalculatorTest extends TestCase
{
    private Calculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }

    public function testAddition(): void
    {
        // 基本的な等価性テスト
        $this->assertEquals(4, $this->calculator->add(2, 2));

        // 浮動小数点数の比較
        $this->assertEqualsWithDelta(0.3, $this->calculator->add(0.1, 0.2), 0.0001);

        // 配列の比較
        $this->assertSame(
            ['total' => 4, 'operation' => 'add'],
            $this->calculator->addWithDetails(2, 2)
        );
    }

    public function testDivisionByZero(): void
    {
        // 例外のテスト
        $this->expectException(DivisionByZeroException::class);
        $this->calculator->divide(10, 0);
    }
}

モックオブジェクトを活用した外部依存のテスト

外部サービスやデータベースに依存するコードをテストする際のモックの活用方法:

class UserServiceTest extends TestCase
{
    public function testUserCreation(): void
    {
        // データベースレポジトリのモック作成
        $repositoryMock = $this->createMock(UserRepository::class);

        // モックの振る舞いを定義
        $repositoryMock->expects($this->once())
            ->method('save')
            ->with($this->callback(function($user) {
                return $user->getEmail() === 'test@example.com';
            }))
            ->willReturn(true);

        // メール送信サービスのモック
        $mailerMock = $this->createMock(MailerInterface::class);
        $mailerMock->expects($this->once())
            ->method('sendWelcomeEmail');

        $service = new UserService($repositoryMock, $mailerMock);
        $result = $service->createUser('test@example.com', 'password123');

        $this->assertTrue($result);
    }
}

データプロバイダーによるパラメータ化テスト

多様なテストケースを効率的に実行するためのデータプロバイダーの活用:

class ValidationTest extends TestCase
{
    /**
     * @dataProvider emailValidationProvider
     */
    public function testEmailValidation(string $email, bool $expectedValid): void
    {
        $validator = new EmailValidator();
        $this->assertEquals($expectedValid, $validator->isValid($email));
    }

    public function emailValidationProvider(): array
    {
        return [
            'valid email' => ['test@example.com', true],
            'missing @' => ['testexample.com', false],
            'invalid domain' => ['test@.com', false],
            'with spaces' => ['test @example.com', false],
            'with special chars' => ['test+filter@example.com', true]
        ];
    }
}

例外処理のテスト手法

例外処理の適切なテスト方法と、より詳細な例外情報の検証:

class ExceptionHandlingTest extends TestCase
{
    public function testSpecificException(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Value must be positive');
        $this->expectExceptionCode(400);

        $calculator = new Calculator();
        $calculator->calculateSquareRoot(-1);
    }

    public function testExceptionWithContext(): void
    {
        try {
            $processor = new DataProcessor();
            $processor->process(['invalid' => 'data']);
            $this->fail('Expected exception was not thrown');
        } catch (ProcessingException $e) {
            $this->assertStringContainsString('invalid data format', $e->getMessage());
            $this->assertNotNull($e->getContext());
            $this->assertArrayHasKey('input', $e->getContext());
        }
    }
}

データベース操作のテスト方法

DBUnit拡張を使用したデータベーステストの実装:

class UserRepositoryTest extends DatabaseTestCase
{
    private UserRepository $repository;

    public function getDataSet(): IDataSet
    {
        return new YamlDataSet(dirname(__FILE__) . '/fixtures/users.yml');
    }

    protected function setUp(): void
    {
        parent::setUp();
        $this->repository = new UserRepository($this->getConnection());
    }

    public function testFindUserById(): void
    {
        $user = $this->repository->find(1);

        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user->getName());
        $this->assertEquals('john@example.com', $user->getEmail());
    }

    public function testCreateUser(): void
    {
        $newUser = new User('Jane Doe', 'jane@example.com');
        $this->repository->save($newUser);

        $this->assertTableRowCount('users', 2);
        $this->assertTableContains(
            'users',
            ['name' => 'Jane Doe', 'email' => 'jane@example.com']
        );
    }
}

非同期処理のテスト実装

非同期処理やPromiseパターンを使用したコードのテスト方法:

class AsyncProcessTest extends TestCase
{
    public function testAsyncOperation(): void
    {
        $processor = new AsyncProcessor();

        $promise = $processor->processAsync('test-data');

        // 非同期処理の完了を待機
        $result = $promise->wait();

        $this->assertNotNull($result);
        $this->assertEquals('processed-test-data', $result->getData());
    }

    public function testMultipleAsyncOperations(): void
    {
        $processor = new AsyncProcessor();

        $promises = [
            'first' => $processor->processAsync('data1'),
            'second' => $processor->processAsync('data2')
        ];

        // 全ての非同期処理の完了を待機
        $results = Promise\Utils::all($promises)->wait();

        $this->assertCount(2, $results);
        $this->assertArrayHasKey('first', $results);
        $this->assertArrayHasKey('second', $results);
    }
}

プライベートメソッドのテストテクニック

プライベートメソッドやプロテクテッドメソッドのテスト方法:

class PrivateMethodTest extends TestCase
{
    public function testPrivateCalculation(): void
    {
        $calculator = new ComplexCalculator();

        // リフレクションを使用してプライベートメソッドにアクセス
        $method = new ReflectionMethod(ComplexCalculator::class, 'calculateIntermediate');
        $method->setAccessible(true);

        $result = $method->invoke($calculator, 10, 5);
        $this->assertEquals(50, $result);
    }

    public function testProtectedPropertyAccess(): void
    {
        $processor = new DataProcessor();

        // リフレクションを使用してプロテクテッドプロパティを設定
        $reflection = new ReflectionClass(DataProcessor::class);
        $property = $reflection->getProperty('configuration');
        $property->setAccessible(true);

        $property->setValue($processor, ['key' => 'value']);

        // 公開メソッドを通じて間接的にテスト
        $result = $processor->process(['data']);
        $this->assertTrue($result->isConfigured());
    }
}

これらのテスト手法を組み合わせることで、堅牢なテストスイートを構築できます。各手法は状況に応じて適切に選択し、テストの保守性と信頼性を確保することが重要です。

PHPUnitテストの設計ベストプラクティス

テストケース設計の原則とTDDの実践方法

テスト駆動開発(TDD)の基本サイクル

  1. Red: 失敗するテストを書く
public function testUserRegistration(): void
{
    $userService = new UserService();
    $user = $userService->register(
        'test@example.com',
        'password123',
        'John Doe'
    );

    $this->assertInstanceOf(User::class, $user);
    $this->assertEquals('test@example.com', $user->getEmail());
    $this->assertTrue($user->isActive());
}
  1. Green: テストが通るように最小限の実装を行う
class UserService
{
    public function register(string $email, string $password, string $name): User
    {
        $user = new User();
        $user->setEmail($email);
        $user->setName($name);
        $user->setPassword(password_hash($password, PASSWORD_BCRYPT));
        $user->setActive(true);

        return $user;
    }
}
  1. Refactor: コードをリファクタリングする
class UserService
{
    private UserRepository $repository;
    private PasswordHasher $passwordHasher;

    public function register(string $email, string $password, string $name): User
    {
        $user = User::create($email, $name);
        $user->setPassword($this->passwordHasher->hash($password));
        $user->activate();

        $this->repository->save($user);
        return $user;
    }
}

FIRST原則の実践

原則説明実装例
Fastテストは高速に実行される外部依存をモック化
Isolatedテストは独立しているテストごとにデータをリセット
Repeatableテストは何度実行しても同じ結果固定のテストデータを使用
Self-validatingテストは自己検証可能アサーションを明確に記述
Timelyテストはコードと同時に書くTDDサイクルを遵守

テストの命名規則とディレクトリ構造の整理

推奨されるディレクトリ構造

tests/
├── Unit/
│   ├── Domain/
│   │   ├── UserTest.php
│   │   └── OrderTest.php
│   └── Service/
│       ├── UserServiceTest.php
│       └── OrderServiceTest.php
├── Integration/
│   ├── Repository/
│   │   └── UserRepositoryTest.php
│   └── Service/
│       └── PaymentServiceTest.php
├── Feature/
│   ├── UserRegistrationTest.php
│   └── OrderProcessTest.php
└── TestCase.php

テストクラスとメソッドの命名規則

// 機能単位でのテストクラス名
class UserRegistrationTest extends TestCase
{
    // 正常系テスト
    public function testSuccessfulRegistration(): void
    {
        // テストコード
    }

    // 異常系テスト
    public function testRegistrationFailsWithInvalidEmail(): void
    {
        // テストコード
    }

    // 状態確認テスト
    public function testUserIsInactiveByDefault(): void
    {
        // テストコード
    }
}

// ドメインモデルのテストクラス名
class UserTest extends TestCase
{
    // 振る舞いベースの命名
    public function testUserCanChangePassword(): void
    {
        // テストコード
    }

    // 状態変更の検証
    public function testUserBecomesInactiveAfterTooManyLoginAttempts(): void
    {
        // テストコード
    }
}

テストカバレッジの測定と活用方法

カバレッジレポートの生成と解析

  1. PHPUnit設定でのカバレッジ設定
<coverage processUncoveredFiles="true">
    <include>
        <directory suffix=".php">src</directory>
    </include>
    <exclude>
        <directory>src/Legacy</directory>
        <file>src/bootstrap.php</file>
    </exclude>
    <report>
        <html outputDirectory="coverage"/>
        <clover outputFile="coverage.xml"/>
        <text outputFile="coverage.txt"/>
    </report>
</coverage>
  1. カバレッジ計測の実行
# HTMLレポート生成
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage/

# テキストレポート生成
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text

カバレッジ目標の設定と監視

class PaymentProcessorTest extends TestCase
{
    /**
     * @coversDefaultClass \App\Service\PaymentProcessor
     * @covers ::process
     */
    public function testPaymentProcessing(): void
    {
        $processor = new PaymentProcessor();
        $result = $processor->process($payment);

        $this->assertTrue($result->isSuccessful());
    }
}

カバレッジ品質の確保

  1. 重要なビジネスロジックの優先的なカバー
/**
 * @covers \App\Service\OrderProcessor::calculateTotal
 * @testWith ["standard", 100, 110]
 *           ["premium", 100, 105]
 *           ["vip", 100, 100]
 */
public function testOrderTotalCalculation(
    string $customerType,
    float $baseAmount,
    float $expectedTotal
): void {
    $processor = new OrderProcessor();
    $actual = $processor->calculateTotal($customerType, $baseAmount);

    $this->assertEquals($expectedTotal, $actual);
}
  1. 境界値テストの重要性
class DiscountCalculatorTest extends TestCase
{
    /**
     * @covers \App\Service\DiscountCalculator::calculateDiscount
     * @dataProvider discountBoundaryProvider
     */
    public function testDiscountBoundaries(
        float $amount,
        float $expectedDiscount
    ): void {
        $calculator = new DiscountCalculator();
        $actual = $calculator->calculateDiscount($amount);

        $this->assertEquals($expectedDiscount, $actual);
    }

    public function discountBoundaryProvider(): array
    {
        return [
            'minimum amount' => [999.99, 0.0],
            'discount threshold' => [1000.0, 100.0],
            'maximum discount' => [10000.0, 500.0]
        ];
    }
}

これらのベストプラクティスを適用することで、保守性が高く、信頼性のあるテストスイートを構築できます。テストの品質を継続的に監視し、改善することで、プロジェクトの品質向上に貢献できます。

CI/CDパイプラインにおけるPHPUnitの活用法

GitHubActionsでの自動テスト設定

GitHub Actionsを使用して、PHPUnitテストを自動化する具体的な実装方法を解説します。

基本的なワークフロー設定

.github/workflows/tests.ymlの実装例:

name: PHP Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php-versions: ['8.1', '8.2']

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v3

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php-versions }}
        extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql
        coverage: xdebug

    - name: Validate composer.json and composer.lock
      run: composer validate --strict

    - name: Cache Composer packages
      id: composer-cache
      uses: actions/cache@v3
      with:
        path: vendor
        key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
        restore-keys: |
          ${{ runner.os }}-php-

    - name: Install dependencies
      run: composer install --prefer-dist --no-progress

    - name: Run test suite
      run: |
        mkdir -p build/logs
        XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover build/logs/clover.xml

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: build/logs/clover.xml
        fail_ci_if_error: true

テスト結果の可視化

GitHubのプルリクエストに自動的にテスト結果を表示する設定:

    - name: Create Test Summary
      if: always()
      uses: test-summary/action@v2
      with:
        paths: build/logs/junit.xml

    - name: Add Coverage Comment
      uses: marocchino/sticky-pull-request-comment@v2
      if: github.event_name == 'pull_request'
      with:
        recreate: true
        path: build/logs/coverage-summary.txt

Jenkins連携によるテスト自動化の実現

Jenkinsfileの実装例

pipeline {
    agent any

    environment {
        PHP_VERSION = '8.2'
        COMPOSER_HOME = "${WORKSPACE}/.composer"
    }

    stages {
        stage('Setup') {
            steps {
                sh 'composer install --no-interaction --no-progress'
            }
        }

        stage('Static Analysis') {
            parallel {
                stage('PHPStan') {
                    steps {
                        sh 'vendor/bin/phpstan analyse src tests'
                    }
                }
                stage('PHP_CodeSniffer') {
                    steps {
                        sh 'vendor/bin/phpcs src tests'
                    }
                }
            }
        }

        stage('Unit Tests') {
            steps {
                sh '''
                    mkdir -p build/logs
                    XDEBUG_MODE=coverage vendor/bin/phpunit \
                        --coverage-clover build/logs/clover.xml \
                        --coverage-html build/coverage
                '''
            }
            post {
                always {
                    publishHTML(
                        target: [
                            reportDir: 'build/coverage',
                            reportFiles: 'index.html',
                            reportName: 'Coverage Report'
                        ]
                    )

                    junit 'build/logs/junit.xml'
                }
            }
        }
    }

    post {
        success {
            slackSend(
                color: 'good',
                message: "テストが成功しました: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'"
            )
        }
        failure {
            slackSend(
                color: 'danger',
                message: "テストが失敗しました: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'"
            )
        }
    }
}

テスト品質管理の自動化

  1. カバレッジしきい値の設定
<!-- phpunit.xml -->
<coverage>
    <report>
        <clover outputFile="build/logs/clover.xml"/>
        <html outputDirectory="build/coverage"/>
    </report>
</coverage>
<logging>
    <junit outputFile="build/logs/junit.xml"/>
</logging>
  1. 品質ゲートの設定
stage('Quality Gate') {
    steps {
        script {
            def coverageResult = sh(
                script: '''
                    coverage=$(grep -A 5 "<metrics" build/logs/clover.xml | \
                    grep "statements" | \
                    cut -d'"' -f4)
                    echo $coverage
                ''',
                returnStdout: true
            ).trim()

            if (coverageResult.toFloat() < 80) {
                error "コードカバレッジが80%未満です: ${coverageResult}%"
            }
        }
    }
}

ビルドパイプラインの最適化

  1. 並列テスト実行
stage('Parallel Tests') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh 'vendor/bin/phpunit --testsuite unit'
            }
        }
        stage('Integration Tests') {
            steps {
                sh 'vendor/bin/phpunit --testsuite integration'
            }
        }
        stage('Feature Tests') {
            steps {
                sh 'vendor/bin/phpunit --testsuite feature'
            }
        }
    }
}
  1. テストの高速化
# docker-compose.ci.yml
version: '3.8'
services:
  php-test:
    build:
      context: .
      dockerfile: Dockerfile.ci
    volumes:
      - .:/app
    environment:
      - APP_ENV=testing
      - DB_CONNECTION=sqlite
      - DB_DATABASE=:memory:

これらの設定により、以下のような継続的インテグレーションフローが実現できます:

  1. コードのプッシュ/PRの作成
  2. 自動テストの実行
  3. コードカバレッジの計測
  4. 品質基準のチェック
  5. テスト結果のレポート生成
  6. 通知の送信

この自動化されたワークフローにより、開発チームは品質を維持しながら、効率的な開発を進めることができます。

PHPUnitでよくあるエラーとトラブルシューティング

環境構築時の主要なエラー対処法

1. Composerインストール関連のエラー

Problem 1
    - phpunit/phpunit[9.0.0, ..., 9.6.13] require php >= 7.3 -> your php version (7.2.34) does not satisfy that requirement.

解決策:

# PHP バージョンの確認
php -v

# 適切なPHPバージョンのインストール(Ubuntu/Debian)
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install php8.2

# PHPUnitの特定バージョンをインストール
composer require --dev phpunit/phpunit:^8.5

2. 設定ファイルの構文エラー

<!-- エラーが発生する設定 -->
<phpunit>
    <testsuites>
        <testsuite>  <!-- name属性が欠落 -->
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

<!-- 正しい設定 -->
<phpunit>
    <testsuites>
        <testsuite name="unit">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

3. テストディレクトリの構造エラー

// エラー: クラス名とファイル名が一致しない
// ファイル名: UserTest.php
class CustomerTest extends TestCase
{
    // テストコード
}

// 正しい実装
// ファイル名: UserTest.php
class UserTest extends TestCase
{
    // テストコード
}

テスト実行時のメモリ管理とパフォーマンス向上

1. メモリ制限の対処

// メモリ使用量を監視するテストケース
class MemoryTest extends TestCase
{
    protected function setUp(): void
    {
        // テスト開始時のメモリ使用量を記録
        $this->initialMemory = memory_get_usage();
    }

    protected function tearDown(): void
    {
        // メモリリークの検出
        $memoryDiff = memory_get_usage() - $this->initialMemory;
        if ($memoryDiff > 1024 * 1024) { // 1MB以上の差がある場合
            $this->fail("メモリリークの可能性: {$memoryDiff} bytes");
        }
    }

    public function testLargeDataSet(): void
    {
        // メモリ制限の一時的な引き上げ
        ini_set('memory_limit', '512M');

        // 大きなデータセットのテスト
        $largeArray = array_fill(0, 1000000, 'data');
        $result = $this->processor->process($largeArray);

        $this->assertNotEmpty($result);
    }
}

2. パフォーマンス最適化テクニック

class PerformanceTest extends TestCase
{
    public function testDatabaseOperations(): void
    {
        // トランザクションを使用してテストを高速化
        $this->connection->beginTransaction();

        try {
            // テストコード
            $user = new User();
            $user->save();

            $this->assertDatabaseHas('users', [
                'id' => $user->id
            ]);
        } finally {
            // テスト後にロールバック
            $this->connection->rollBack();
        }
    }

    /**
     * @group slow
     */
    public function testTimeConsumingOperation(): void
    {
        // 時間のかかるテストを分離
        $startTime = microtime(true);

        // テスト実行
        $result = $this->service->processLargeData();

        $executionTime = microtime(true) - $startTime;
        $this->assertLessThan(
            5.0,
            $executionTime,
            "処理に{$executionTime}秒かかりました"
        );
    }
}

非互換性の解決とバージョンアップ時の注意点

1. PHPUnit 9.xから10.xへの移行対応

// PHPUnit 9.x での書き方
class LegacyTest extends TestCase
{
    public function testException(): void
    {
        $this->expectException(Exception::class);
        throw new Exception('エラー');
    }

    public function testOutput(): void
    {
        $this->expectOutputString('期待する出力');
        echo '期待する出力';
    }
}

// PHPUnit 10.x での書き方
class ModernTest extends TestCase
{
    public function testException(): void
    {
        $this->expectException(Exception::class);
        $this->expectExceptionMessage('エラー');
        throw new Exception('エラー');
    }

    public function testOutput(): void
    {
        $this->expectOutputString('期待する出力');
        echo '期待する出力';
    }
}

2. 非推奨機能の置き換え

// 非推奨の書き方
class DeprecatedTest extends TestCase
{
    public function testOldStyle(): void
    {
        $this->assertInternalType('string', $value);
        $this->assertAttributeEquals('expected', 'property', $object);
    }
}

// 推奨される書き方
class ModernTest extends TestCase
{
    public function testNewStyle(): void
    {
        $this->assertIsString($value);
        $this->assertEquals('expected', $object->property);
    }
}

3. バージョンアップ時のチェックリスト

class VersionCompatibilityTest extends TestCase
{
    public function setUp(): void
    {
        // PHPUnitバージョンの確認
        if (version_compare(PHPUnit\Runner\Version::id(), '10.0.0', '>=')) {
            // PHPUnit 10以降の設定
            $this->markTestSkipped('このテストはPHPUnit 10以降では非対応');
        }
    }

    /**
     * @requires PHP >= 8.1
     */
    public function testPhp81Feature(): void
    {
        // PHP 8.1以降の機能をテスト
        $result = str_contains($haystack, $needle);
        $this->assertTrue($result);
    }
}

主要なトラブルシューティングのチェックリスト

  1. メモリ関連の問題
  • メモリ制限の確認と調整
  • 大きなデータセットの分割処理
  • リソースの適切な解放
  1. パフォーマンス問題
  • テストの分離と並列実行
  • データベーストランザクションの活用
  • テストスイートの最適な構成
  1. 互換性の問題
  • PHPバージョンの確認
  • PHPUnit バージョンの確認
  • 依存ライブラリの互換性確認

これらの問題に遭遇した場合、上記の解決策を順次試していくことで、多くの場合問題を解決できます。

実践的なPHPUnitテストコード例とサンプルプロジェクト

ECサイトのショッピングカート機能のテスト実装例

1. ショッピングカートのドメインモデル

class Cart
{
    private array $items = [];
    private ?DiscountCode $discountCode = null;

    public function addItem(Product $product, int $quantity): void
    {
        $this->items[] = new CartItem($product, $quantity);
    }

    public function removeItem(string $productId): void
    {
        $this->items = array_filter(
            $this->items,
            fn($item) => $item->getProduct()->getId() !== $productId
        );
    }

    public function applyDiscountCode(DiscountCode $code): void
    {
        $this->discountCode = $code;
    }

    public function calculateTotal(): float
    {
        $subtotal = array_reduce(
            $this->items,
            fn($total, $item) => $total + $item->getSubtotal(),
            0.0
        );

        if ($this->discountCode) {
            return $subtotal * (1 - $this->discountCode->getDiscountRate());
        }

        return $subtotal;
    }
}

2. カートのユニットテスト

class CartTest extends TestCase
{
    private Cart $cart;
    private Product $product;

    protected function setUp(): void
    {
        $this->cart = new Cart();
        $this->product = new Product(
            'prod-001',
            'テスト商品',
            1000,
            10 // 在庫数
        );
    }

    /**
     * @test
     */
    public function 商品を追加できること(): void
    {
        // 実行
        $this->cart->addItem($this->product, 2);

        // 検証
        $this->assertEquals(2000, $this->cart->calculateTotal());
    }

    /**
     * @test
     */
    public function 商品を削除できること(): void
    {
        // 準備
        $this->cart->addItem($this->product, 2);

        // 実行
        $this->cart->removeItem('prod-001');

        // 検証
        $this->assertEquals(0, $this->cart->calculateTotal());
    }

    /**
     * @test
     */
    public function 割引コードを適用できること(): void
    {
        // 準備
        $this->cart->addItem($this->product, 2);
        $discountCode = new DiscountCode('SAVE10', 0.1); // 10%割引

        // 実行
        $this->cart->applyDiscountCode($discountCode);

        // 検証
        $this->assertEquals(1800, $this->cart->calculateTotal());
    }

    /**
     * @test
     */
    public function 在庫数以上の商品は追加できないこと(): void
    {
        $this->expectException(InvalidQuantityException::class);
        $this->cart->addItem($this->product, 11);
    }
}

ユーザー認証システムのテストケース作成

1. 認証サービスの実装

class AuthenticationService
{
    private UserRepository $userRepository;
    private PasswordHasher $passwordHasher;
    private TokenGenerator $tokenGenerator;

    public function __construct(
        UserRepository $userRepository,
        PasswordHasher $passwordHasher,
        TokenGenerator $tokenGenerator
    ) {
        $this->userRepository = $userRepository;
        $this->passwordHasher = $passwordHasher;
        $this->tokenGenerator = $tokenGenerator;
    }

    public function authenticate(string $email, string $password): AuthResult
    {
        $user = $this->userRepository->findByEmail($email);

        if (!$user) {
            throw new UserNotFoundException();
        }

        if (!$this->passwordHasher->verify($password, $user->getPasswordHash())) {
            throw new InvalidPasswordException();
        }

        if (!$user->isActive()) {
            throw new InactiveUserException();
        }

        return new AuthResult(
            $user,
            $this->tokenGenerator->generate($user)
        );
    }
}

2. 認証サービスのテスト

class AuthenticationServiceTest extends TestCase
{
    private AuthenticationService $service;
    private MockObject $userRepository;
    private MockObject $passwordHasher;
    private MockObject $tokenGenerator;

    protected function setUp(): void
    {
        $this->userRepository = $this->createMock(UserRepository::class);
        $this->passwordHasher = $this->createMock(PasswordHasher::class);
        $this->tokenGenerator = $this->createMock(TokenGenerator::class);

        $this->service = new AuthenticationService(
            $this->userRepository,
            $this->passwordHasher,
            $this->tokenGenerator
        );
    }

    /**
     * @test
     */
    public function 正しい認証情報で認証できること(): void
    {
        // 準備
        $email = 'test@example.com';
        $password = 'password123';
        $hashedPassword = 'hashed_password';
        $token = 'generated_token';

        $user = new User($email, $hashedPassword);
        $user->activate();

        $this->userRepository
            ->expects($this->once())
            ->method('findByEmail')
            ->with($email)
            ->willReturn($user);

        $this->passwordHasher
            ->expects($this->once())
            ->method('verify')
            ->with($password, $hashedPassword)
            ->willReturn(true);

        $this->tokenGenerator
            ->expects($this->once())
            ->method('generate')
            ->with($user)
            ->willReturn($token);

        // 実行
        $result = $this->service->authenticate($email, $password);

        // 検証
        $this->assertInstanceOf(AuthResult::class, $result);
        $this->assertSame($user, $result->getUser());
        $this->assertEquals($token, $result->getToken());
    }

    /**
     * @test
     */
    public function 存在しないユーザーの場合は例外が発生すること(): void
    {
        $this->userRepository
            ->method('findByEmail')
            ->willReturn(null);

        $this->expectException(UserNotFoundException::class);
        $this->service->authenticate('invalid@example.com', 'password');
    }
}

APIエンドポイントのテスト実装方法

1. RESTful APIコントローラー

class ProductApiController
{
    private ProductRepository $repository;
    private ProductValidator $validator;

    public function create(Request $request): JsonResponse
    {
        try {
            $data = $request->getJsonData();

            $this->validator->validate($data);

            $product = new Product(
                $data['name'],
                $data['price'],
                $data['description']
            );

            $savedProduct = $this->repository->save($product);

            return new JsonResponse(
                ['id' => $savedProduct->getId()],
                201
            );
        } catch (ValidationException $e) {
            return new JsonResponse(
                ['errors' => $e->getErrors()],
                400
            );
        } catch (Exception $e) {
            return new JsonResponse(
                ['error' => 'Internal Server Error'],
                500
            );
        }
    }
}

2. APIエンドポイントのテスト

class ProductApiControllerTest extends TestCase
{
    private ProductApiController $controller;
    private MockObject $repository;
    private MockObject $validator;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(ProductRepository::class);
        $this->validator = $this->createMock(ProductValidator::class);
        $this->controller = new ProductApiController(
            $this->repository,
            $this->validator
        );
    }

    /**
     * @test
     */
    public function 正常な商品登録ができること(): void
    {
        // 準備
        $requestData = [
            'name' => 'テスト商品',
            'price' => 1000,
            'description' => '商品の説明'
        ];

        $request = new Request();
        $request->setJsonData($requestData);

        $savedProduct = new Product(
            $requestData['name'],
            $requestData['price'],
            $requestData['description']
        );
        $savedProduct->setId('prod-001');

        $this->validator
            ->expects($this->once())
            ->method('validate')
            ->with($requestData);

        $this->repository
            ->expects($this->once())
            ->method('save')
            ->willReturn($savedProduct);

        // 実行
        $response = $this->controller->create($request);

        // 検証
        $this->assertEquals(201, $response->getStatusCode());
        $this->assertEquals(
            ['id' => 'prod-001'],
            $response->getJsonData()
        );
    }

    /**
     * @test
     */
    public function バリデーションエラー時は400エラーを返すこと(): void
    {
        // 準備
        $requestData = [
            'name' => '', // 空の名前は無効
            'price' => -100, // 負の価格は無効
            'description' => 'OK'
        ];

        $request = new Request();
        $request->setJsonData($requestData);

        $this->validator
            ->method('validate')
            ->willThrowException(new ValidationException([
                'name' => ['商品名は必須です'],
                'price' => ['価格は0以上である必要があります']
            ]));

        // 実行
        $response = $this->controller->create($request);

        // 検証
        $this->assertEquals(400, $response->getStatusCode());
        $this->assertArrayHasKey('errors', $response->getJsonData());
    }
}

これらの実装例は、実際のプロジェクトでよく使用される機能のテストパターンを示しています。テストコードは、機能の仕様を明確に示すドキュメントとしても機能し、新しいチームメンバーの参考資料としても活用できます。