PHPUnit とは:Web アプリケーションの品質を支えるテストフレームワーク
PHPUnitは、PHP言語用の単体テストフレームワークとして最も広く使用されているツールです。Sebastian Bergmannによって開発され、xUnitアーキテクチャに基づいて設計されています。多くの主要なPHPプロジェクトやフレームワーク(Laravel、Symfony、WordPressなど)で採用されており、信頼性の高いテスト環境を提供します。
PHPUnit が解決する3つの開発現場の課題
- 品質保証の効率化と自動化
- 手動テストによる人的ミスの削減
- 継続的なリグレッションテストの実現
- テスト実行時間の大幅な短縮
// 従来の手動テスト $result = someFunction(); if ($result === expected) { echo "Test passed"; } // PHPUnitを使用した自動化テスト public function testSomeFunction(): void { $this->assertEquals(expected, someFunction()); }
- 開発速度と品質の両立
- テストファーストな開発アプローチの実現
- 早期のバグ発見による修正コストの削減
- リファクタリングの安全性確保
// テストファーストな開発例 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()); }
- チーム開発におけるコード品質の標準化
- 統一されたテスト基準の確立
- コードレビューの効率化
- 新規メンバーの学習曲線の緩和
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を使用することです。以下に、段階的なインストール手順を示します。
- 事前準備
# プロジェクトディレクトリの作成と移動 mkdir my-project cd my-project # Composerプロジェクトの初期化 composer init --require="phpunit/phpunit:^10.0" --dev
- PHPUnitのインストール
# PHPUnitをdev依存関係としてインストール composer require --dev phpunit/phpunit ^10.0 # インストールの確認 ./vendor/bin/phpunit --version
- プロジェクト構造のセットアップ
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 での設定
- テスト実行環境の設定
Settings > Languages & Frameworks > PHP > Test Frameworks
- PHPUnitのパスを
vendor/autoload.php
に設定 - テスト設定ファイル(phpunit.xml)のパスを指定
- ショートカットキーの活用
Ctrl + Shift + T
:テストクラスの作成Ctrl + Shift + F10
:カーソル位置のテスト実行Alt + Shift + F10
:テスト実行設定の選択
2. Visual Studio Code での設定
- 推奨拡張機能のインストール
- PHP Extension Pack
- PHPUnit Test Explorer
- settings.jsonの設定例
{ "php.validate.enable": true, "phpunit.phpunit": "./vendor/bin/phpunit", "phpunit.args": [ "--configuration", "./phpunit.xml" ] }
3. テスト実行の効率化テクニック
- ファイルウォッチャーの設定
# Laravel Mix使用時の例 mix.browserSync('localhost:8000') .version() .watch(['tests/**/*.php'], () => { exec('./vendor/bin/phpunit'); });
- テストフィルタリング
# 特定のテストグループのみ実行 ./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)の基本サイクル
- 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()); }
- 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; } }
- 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 { // テストコード } }
テストカバレッジの測定と活用方法
カバレッジレポートの生成と解析
- 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>
- カバレッジ計測の実行
# 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()); } }
カバレッジ品質の確保
- 重要なビジネスロジックの優先的なカバー
/** * @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); }
- 境界値テストの重要性
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}]'" ) } } }
テスト品質管理の自動化
- カバレッジしきい値の設定
<!-- phpunit.xml --> <coverage> <report> <clover outputFile="build/logs/clover.xml"/> <html outputDirectory="build/coverage"/> </report> </coverage> <logging> <junit outputFile="build/logs/junit.xml"/> </logging>
- 品質ゲートの設定
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}%" } } } }
ビルドパイプラインの最適化
- 並列テスト実行
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' } } } }
- テストの高速化
# 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:
これらの設定により、以下のような継続的インテグレーションフローが実現できます:
- コードのプッシュ/PRの作成
- 自動テストの実行
- コードカバレッジの計測
- 品質基準のチェック
- テスト結果のレポート生成
- 通知の送信
この自動化されたワークフローにより、開発チームは品質を維持しながら、効率的な開発を進めることができます。
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); } }
主要なトラブルシューティングのチェックリスト
- メモリ関連の問題
- メモリ制限の確認と調整
- 大きなデータセットの分割処理
- リソースの適切な解放
- パフォーマンス問題
- テストの分離と並列実行
- データベーストランザクションの活用
- テストスイートの最適な構成
- 互換性の問題
- 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()); } }
これらの実装例は、実際のプロジェクトでよく使用される機能のテストパターンを示しています。テストコードは、機能の仕様を明確に示すドキュメントとしても機能し、新しいチームメンバーの参考資料としても活用できます。