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