【完全ガイド】PHPUnitで実現する堅牢なコード品質 – 導入から実践まで7つのステップ

目次

目次へ

目次

PHPUnitとは?基本概念と重要性を理解する

PHPUnitは、PHP言語で開発されたアプリケーションのための単体テストフレームワークであり、現在のPHP開発において最も広く使われているテストツールです。Sebastian Bergmannによって開発され、xUnitファミリーに属するこのフレームワークは、継続的な改善を重ね、PHP開発の品質保証において欠かせない存在となっています。

PHPUnitがPHP開発の標準テストフレームワークである理由

PHPUnitがPHP開発における事実上の標準テストフレームワークとなった理由には、いくつかの重要な要素があります:

  • 成熟度と安定性: 2004年の初リリース以来、長期にわたる開発と改善を経ており、安定性と信頼性が高い
  • 広範なコミュニティサポート: 活発なコミュニティによる継続的な改善とドキュメント整備
  • 主要フレームワークとの統合: Laravel、Symfony、CakePHPなど主要なPHPフレームワークが標準でサポート
  • 豊富な機能セット: アサーション、モック、スタブ、データプロバイダーなど、包括的なテスト機能を提供
  • 自動化ツールとの互換性: JenkinsやGitHub Actionsなど、CI/CDツールとの連携が容易

テスト駆動開発(TDD)とPHPUnitの相性

PHPUnitは、テスト駆動開発(TDD)のワークフローと非常に相性が良いフレームワークです。TDDの基本サイクルは次のとおりです:

  1. Red: まず失敗するテストを書く
  2. Green: テストが成功するように最小限のコードを実装する
  3. Refactor: コードをリファクタリングしながらテストが成功し続けることを確認する

PHPUnitはこのサイクルを効率的に回すための機能を備えています:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 例:TDDアプローチでユーザークラスをテスト
class UserTest extends PHPUnit\Framework\TestCase
{
public function testUserHasName()
{
// 1. Red: テストを書く(この時点ではUserクラスは実装されていない)
$user = new User('John Doe');
$this->assertEquals('John Doe', $user->getName());
}
}
// 例:TDDアプローチでユーザークラスをテスト class UserTest extends PHPUnit\Framework\TestCase { public function testUserHasName() { // 1. Red: テストを書く(この時点ではUserクラスは実装されていない) $user = new User('John Doe'); $this->assertEquals('John Doe', $user->getName()); } }
// 例:TDDアプローチでユーザークラスをテスト
class UserTest extends PHPUnit\Framework\TestCase
{
    public function testUserHasName()
    {
        // 1. Red: テストを書く(この時点ではUserクラスは実装されていない)
        $user = new User('John Doe');
        $this->assertEquals('John Doe', $user->getName());
    }
}

このようにテストを先に書き、その後実装を行うことで、明確な要件定義とバグの少ないコード開発が可能になります。

単体テストがもたらすコード品質と開発効率の向上

PHPUnitを用いた単体テストは、以下のような多くのメリットをもたらします:

メリット説明
バグの早期発見開発初期段階でバグを検出し、修正コストを低減
回帰テスト既存機能が新機能追加によって壊れていないことを確認
リファクタリングの安全性コード改善時に機能が保持されていることを確認
ドキュメントとしての役割テストコードが仕様書としても機能し、他の開発者の理解を助ける
設計の改善テスト容易性を考慮した設計で、結合度の低いモジュール化されたコードを促進

例えば、PHPUnitを使って計算クラスをテストする場合:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 電卓クラスのテスト例
public function testAddition()
{
$calculator = new Calculator();
$this->assertEquals(5, $calculator->add(2, 3));
// このテストは加算機能の仕様を明確に示している
}
// 電卓クラスのテスト例 public function testAddition() { $calculator = new Calculator(); $this->assertEquals(5, $calculator->add(2, 3)); // このテストは加算機能の仕様を明確に示している }
// 電卓クラスのテスト例
public function testAddition()
{
    $calculator = new Calculator();
    $this->assertEquals(5, $calculator->add(2, 3));
    // このテストは加算機能の仕様を明確に示している
}

このように、PHPUnitはコードが意図したとおりに動作することを保証するだけでなく、開発プロセス全体を改善し、より堅牢で保守性の高いPHPアプリケーションの開発をサポートします。

PHPUnitの環境構築 – 迅速にセットアップする方法

PHPUnit導入の第一歩は適切な環境構築です。ここでは、迅速かつ確実にPHPUnitをセットアップする方法を解説します。

Composerを使った最新版PHPUnitのインストール手順

Composerは現代のPHP開発において標準的なパッケージ管理ツールであり、PHPUnitのインストールにも最適です。プロジェクトにPHPUnitを導入するには、以下の手順に従います。

プロジェクト単位でのインストール(推奨):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# プロジェクトのルートディレクトリで実行
composer require --dev phpunit/phpunit ^10.0
# プロジェクトのルートディレクトリで実行 composer require --dev phpunit/phpunit ^10.0
# プロジェクトのルートディレクトリで実行
composer require --dev phpunit/phpunit ^10.0

このコマンドにより、開発環境用の依存関係としてPHPUnit 10.xがインストールされます。インストール後は以下のコマンドで実行できます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# vendorディレクトリ内のPHPUnitを使用
./vendor/bin/phpunit
# vendorディレクトリ内のPHPUnitを使用 ./vendor/bin/phpunit
# vendorディレクトリ内のPHPUnitを使用
./vendor/bin/phpunit

グローバルインストール(必要な場合):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
composer global require phpunit/phpunit ^10.0
composer global require phpunit/phpunit ^10.0
composer global require phpunit/phpunit ^10.0

グローバルインストールの場合は、以下のコマンドで実行できます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
phpunit
phpunit
phpunit

PHPUnitの基本設定ファイルphpunit.xmlの書き方

PHPUnitの設定はphpunit.xmlまたはphpunit.xml.distファイルに記述します。このファイルはプロジェクトのルートディレクトリに配置するのが一般的です。

基本的なphpunit.xmlの例:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<!-- テストスイートの定義 -->
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<!-- テストカバレッジ設定 -->
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<!-- PHP設定 -->
<php>
<env name="APP_ENV" value="testing"/>
<ini name="display_errors" value="true"/>
</php>
</phpunit>
<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true"> <!-- テストスイートの定義 --> <testsuites> <testsuite name="Project Test Suite"> <directory>tests</directory> </testsuite> </testsuites> <!-- テストカバレッジ設定 --> <coverage> <include> <directory suffix=".php">src</directory> </include> </coverage> <!-- PHP設定 --> <php> <env name="APP_ENV" value="testing"/> <ini name="display_errors" value="true"/> </php> </phpunit>
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <!-- テストスイートの定義 -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    
    <!-- テストカバレッジ設定 -->
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </coverage>
    
    <!-- PHP設定 -->
    <php>
        <env name="APP_ENV" value="testing"/>
        <ini name="display_errors" value="true"/>
    </php>
</phpunit>

主な設定項目は以下の通りです:

設定項目説明
bootstrapオートローダーなど、テスト前に読み込むファイル
testsuitesテストディレクトリやファイルの指定
coverageコードカバレッジ計測の設定
phpテスト実行時のPHP環境変数や設定

異なるPHPバージョンでの互換性と注意点

PHPUnitのバージョンとPHPのバージョンには互換性があり、適切な組み合わせを選択することが重要です:

PHPバージョン推奨PHPUnitバージョン
PHP 8.3/8.2PHPUnit 10.x
PHP 8.1/8.0PHPUnit 9.x
PHP 7.4/7.3PHPUnit 8.x
PHP 7.2/7.1PHPUnit 7.x

注意点:

  • 最新のPHPUnit 10系はPHP 8.1以上が必要です
  • 古いPHPバージョンを使用している場合は、対応するPHPUnitバージョンを明示的に指定してインストールしてください
  • バージョン間では構文や機能に差異があるため、移行時にはテストコードの修正が必要になる場合があります

環境構築が完了したら、次のステップはテストケースの作成です。

PHPUnitの基本的な使い方 – 最初のテストケース作成

環境構築が完了したら、いよいよPHPUnitを使った最初のテストケースを作成していきましょう。ここではテストクラスの基本構造から、テストの実行方法、結果の確認まで順を追って解説します。

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

PHPUnitのテストクラスは、PHPUnit\Framework\TestCaseクラスを継承して作成します。命名規則と基本構造は以下のとおりです:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
// ファイル名: CalculatorTest.php
// クラス名はテスト対象+'Test'とするのが一般的
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
// テスト前の準備処理(各テストメソッド実行前に呼ばれる)
protected function setUp(): void
{
// テストに必要なオブジェクトの初期化など
}
// テスト後の後片付け処理(各テストメソッド実行後に呼ばれる)
protected function tearDown(): void
{
// テストで作成したリソースの解放など
}
// テストメソッド: 'test'で始まるメソッド名
public function testAddition()
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
// または @test アノテーションを使用
/** @test */
public function multiplicationWorks()
{
$calculator = new Calculator();
$result = $calculator->multiply(2, 3);
$this->assertEquals(6, $result);
}
}
<?php // ファイル名: CalculatorTest.php // クラス名はテスト対象+'Test'とするのが一般的 use PHPUnit\Framework\TestCase; class CalculatorTest extends TestCase { // テスト前の準備処理(各テストメソッド実行前に呼ばれる) protected function setUp(): void { // テストに必要なオブジェクトの初期化など } // テスト後の後片付け処理(各テストメソッド実行後に呼ばれる) protected function tearDown(): void { // テストで作成したリソースの解放など } // テストメソッド: 'test'で始まるメソッド名 public function testAddition() { $calculator = new Calculator(); $result = $calculator->add(2, 3); $this->assertEquals(5, $result); } // または @test アノテーションを使用 /** @test */ public function multiplicationWorks() { $calculator = new Calculator(); $result = $calculator->multiply(2, 3); $this->assertEquals(6, $result); } }
<?php
// ファイル名: CalculatorTest.php
// クラス名はテスト対象+'Test'とするのが一般的
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    // テスト前の準備処理(各テストメソッド実行前に呼ばれる)
    protected function setUp(): void
    {
        // テストに必要なオブジェクトの初期化など
    }
    
    // テスト後の後片付け処理(各テストメソッド実行後に呼ばれる)
    protected function tearDown(): void
    {
        // テストで作成したリソースの解放など
    }
    
    // テストメソッド: 'test'で始まるメソッド名
    public function testAddition()
    {
        $calculator = new Calculator();
        $result = $calculator->add(2, 3);
        $this->assertEquals(5, $result);
    }
    
    // または @test アノテーションを使用
    /** @test */
    public function multiplicationWorks()
    {
        $calculator = new Calculator();
        $result = $calculator->multiply(2, 3);
        $this->assertEquals(6, $result);
    }
}

テストメソッドの命名には以下のルールがあります:

  • 「test」で始まるメソッド名
  • または @test アノテーションを使用したメソッド
  • メソッド名は動作を説明する意味のある名前が望ましい(例:testUserCanLogin

assertメソッドを使った様々な検証方法

PHPUnitでは様々なassertメソッドを使って、テスト対象の期待値を検証します。代表的なアサーションは以下のとおりです:

アサーションメソッド用途
assertEquals($expected, $actual)値が等しいか検証(==)$this->assertEquals(5, $sum);
assertSame($expected, $actual)値と型が等しいか検証(===)$this->assertSame(true, $isValid);
assertTrue($condition)条件が true か検証$this->assertTrue($user->isActive());
assertFalse($condition)条件が false か検証$this->assertFalse($cart->isEmpty());
assertNull($actual)値が null か検証$this->assertNull($result);
assertInstanceOf($expected, $actual)オブジェクトが指定クラスのインスタンスか検証$this->assertInstanceOf(User::class, $user);
assertContains($needle, $haystack)配列・イテラブルに要素が含まれるか検証$this->assertContains('apple', $fruits);
assertCount($expectedCount, $haystack)配列・イテラブルの要素数を検証$this->assertCount(3, $users);
assertStringContainsString($needle, $haystack)文字列が特定の部分文字列を含むか検証$this->assertStringContainsString('PHP', $title);

テストでデータを繰り返し使用する場合は、データプロバイダーが便利です:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/**
* @dataProvider additionProvider
*/
public function testAddition($a, $b, $expected)
{
$calculator = new Calculator();
$this->assertEquals($expected, $calculator->add($a, $b));
}
// データプロバイダーメソッド - テストデータを提供
public function additionProvider()
{
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'mixed numbers' => [1, -2, -1]
];
}
/** * @dataProvider additionProvider */ public function testAddition($a, $b, $expected) { $calculator = new Calculator(); $this->assertEquals($expected, $calculator->add($a, $b)); } // データプロバイダーメソッド - テストデータを提供 public function additionProvider() { return [ 'positive numbers' => [1, 2, 3], 'negative numbers' => [-1, -2, -3], 'mixed numbers' => [1, -2, -1] ]; }
/**
 * @dataProvider additionProvider
 */
public function testAddition($a, $b, $expected)
{
    $calculator = new Calculator();
    $this->assertEquals($expected, $calculator->add($a, $b));
}

// データプロバイダーメソッド - テストデータを提供
public function additionProvider()
{
    return [
        'positive numbers' => [1, 2, 3],
        'negative numbers' => [-1, -2, -3],
        'mixed numbers' => [1, -2, -1]
    ];
}

テスト実行とレポート出力のコマンドオプション

PHPUnitのテストは、コマンドラインから実行できます。主なコマンドオプションは以下のとおりです:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 基本的な実行方法
./vendor/bin/phpunit
# 特定のテストファイルを実行
./vendor/bin/phpunit tests/CalculatorTest.php
# 特定のテストメソッドだけを実行
./vendor/bin/phpunit --filter testAddition CalculatorTest
# グループ単位で実行 (@group アノテーションで定義)
./vendor/bin/phpunit --group slow
# テストカバレッジレポートをHTML形式で出力
./vendor/bin/phpunit --coverage-html ./coverage
# 基本的な実行方法 ./vendor/bin/phpunit # 特定のテストファイルを実行 ./vendor/bin/phpunit tests/CalculatorTest.php # 特定のテストメソッドだけを実行 ./vendor/bin/phpunit --filter testAddition CalculatorTest # グループ単位で実行 (@group アノテーションで定義) ./vendor/bin/phpunit --group slow # テストカバレッジレポートをHTML形式で出力 ./vendor/bin/phpunit --coverage-html ./coverage
# 基本的な実行方法
./vendor/bin/phpunit

# 特定のテストファイルを実行
./vendor/bin/phpunit tests/CalculatorTest.php

# 特定のテストメソッドだけを実行
./vendor/bin/phpunit --filter testAddition CalculatorTest

# グループ単位で実行 (@group アノテーションで定義)
./vendor/bin/phpunit --group slow

# テストカバレッジレポートをHTML形式で出力
./vendor/bin/phpunit --coverage-html ./coverage

テスト実行結果は、デフォルトではコンソールに表示されます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
PHPUnit 10.0.0 by Sebastian Bergmann and contributors.
....F. 6 / 6 (100%)
Time: 00:00.003, Memory: 4.00 MB
There was 1 failure:
1) CalculatorTest::testDivision
Failed asserting that 2 is identical to '2'.
FAILURES!
Tests: 6, Assertions: 6, Failures: 1.
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. ....F. 6 / 6 (100%) Time: 00:00.003, Memory: 4.00 MB There was 1 failure: 1) CalculatorTest::testDivision Failed asserting that 2 is identical to '2'. FAILURES! Tests: 6, Assertions: 6, Failures: 1.
PHPUnit 10.0.0 by Sebastian Bergmann and contributors.

....F.                                                              6 / 6 (100%)

Time: 00:00.003, Memory: 4.00 MB

There was 1 failure:

1) CalculatorTest::testDivision
Failed asserting that 2 is identical to '2'.

FAILURES!
Tests: 6, Assertions: 6, Failures: 1.

上記の実行結果では、6つのテストのうち1つが失敗していることがわかります。実行結果は以下の記号で示されます:

  • . (ドット): テスト成功
  • F: テスト失敗
  • E: テスト中にエラー発生
  • S: テストがスキップされた
  • I: テストが不完全または実装されていない

テスト結果に基づいてコードを修正し、すべてのテストが成功するまで継続的に改善していくことがPHPUnitを使った開発の基本的なサイクルです。

PHPUnitを使ったモックとスタブの活用法

実際のアプリケーション開発では、テスト対象のクラスが外部のデータベース、API、ファイルシステムなどに依存することが一般的です。これらの外部依存をテスト時に実際に使用すると、テストが遅くなったり不安定になったりする問題があります。PHPUnitのモックとスタブはこの問題を解決し、外部依存を持つコードを効率的にテストするための強力な機能です。

外部依存を持つコードを効果的にテストする方法

モックとスタブは両方ともテストダブル(テスト用の代役)ですが、その使い方には違いがあります:

  • スタブ: 特定のメソッド呼び出しに対して定義された応答を返すオブジェクト
  • モック: スタブの機能に加えて、期待される動作(メソッドが呼ばれるかどうか、何回呼ばれるかなど)を検証する機能も持つオブジェクト

以下は、PHPUnitでモックオブジェクトを作成する基本的なアプローチです:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// UserServiceクラスのモック化例
public function testNotifyUser()
{
// モックオブジェクトの作成
$emailService = $this->createMock(EmailService::class);
// モックの振る舞いを設定
$emailService->expects($this->once()) // メソッドが1回だけ呼ばれることを期待
->method('sendEmail') // sendEmailメソッドに対して
->with( // 特定の引数で呼ばれることを期待
$this->equalTo('user@example.com'),
$this->anything() // 2番目の引数は何でもOK
)
->willReturn(true); // trueを返すように設定
// モックを注入してテスト対象を実行
$userService = new UserService($emailService);
$result = $userService->notifyUser('user@example.com', 'Hello!');
$this->assertTrue($result);
}
// UserServiceクラスのモック化例 public function testNotifyUser() { // モックオブジェクトの作成 $emailService = $this->createMock(EmailService::class); // モックの振る舞いを設定 $emailService->expects($this->once()) // メソッドが1回だけ呼ばれることを期待 ->method('sendEmail') // sendEmailメソッドに対して ->with( // 特定の引数で呼ばれることを期待 $this->equalTo('user@example.com'), $this->anything() // 2番目の引数は何でもOK ) ->willReturn(true); // trueを返すように設定 // モックを注入してテスト対象を実行 $userService = new UserService($emailService); $result = $userService->notifyUser('user@example.com', 'Hello!'); $this->assertTrue($result); }
// UserServiceクラスのモック化例
public function testNotifyUser()
{
    // モックオブジェクトの作成
    $emailService = $this->createMock(EmailService::class);
    
    // モックの振る舞いを設定
    $emailService->expects($this->once()) // メソッドが1回だけ呼ばれることを期待
               ->method('sendEmail')      // sendEmailメソッドに対して
               ->with(                    // 特定の引数で呼ばれることを期待
                   $this->equalTo('user@example.com'),
                   $this->anything()      // 2番目の引数は何でもOK
               )
               ->willReturn(true);        // trueを返すように設定
    
    // モックを注入してテスト対象を実行
    $userService = new UserService($emailService);
    $result = $userService->notifyUser('user@example.com', 'Hello!');
    
    $this->assertTrue($result);
}

より詳細なモック設定が必要な場合は、getMockBuilderを使用します:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$emailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor() // コンストラクタを無効化
->onlyMethods(['sendEmail']) // モック化するメソッドを指定
->getMock();
$emailService = $this->getMockBuilder(EmailService::class) ->disableOriginalConstructor() // コンストラクタを無効化 ->onlyMethods(['sendEmail']) // モック化するメソッドを指定 ->getMock();
$emailService = $this->getMockBuilder(EmailService::class)
                     ->disableOriginalConstructor() // コンストラクタを無効化
                     ->onlyMethods(['sendEmail'])   // モック化するメソッドを指定
                     ->getMock();

DatabaseやAPIアクセスのモック化テクニック

データベースアクセスやAPIリクエストなどの外部依存のモック化は、特に重要です。以下は主なモック化テクニックです:

データベースアクセスのモック化:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// リポジトリクラスのモック化例
public function testUserRegistration()
{
// リポジトリモックの作成と設定
$userRepository = $this->createMock(UserRepository::class);
$userRepository->method('findByEmail')
->with('new@example.com')
->willReturn(null); // 既存ユーザーがいないと仮定
$userRepository->method('save')
->will($this->returnCallback(function($user) {
// 保存処理をシミュレート
$user->setId(123); // IDを設定
return true;
}));
// テスト対象のクラスにモックを注入
$userService = new UserService($userRepository);
$newUser = $userService->register('new@example.com', 'password123');
$this->assertEquals(123, $newUser->getId());
}
// リポジトリクラスのモック化例 public function testUserRegistration() { // リポジトリモックの作成と設定 $userRepository = $this->createMock(UserRepository::class); $userRepository->method('findByEmail') ->with('new@example.com') ->willReturn(null); // 既存ユーザーがいないと仮定 $userRepository->method('save') ->will($this->returnCallback(function($user) { // 保存処理をシミュレート $user->setId(123); // IDを設定 return true; })); // テスト対象のクラスにモックを注入 $userService = new UserService($userRepository); $newUser = $userService->register('new@example.com', 'password123'); $this->assertEquals(123, $newUser->getId()); }
// リポジトリクラスのモック化例
public function testUserRegistration()
{
    // リポジトリモックの作成と設定
    $userRepository = $this->createMock(UserRepository::class);
    $userRepository->method('findByEmail')
                  ->with('new@example.com')
                  ->willReturn(null); // 既存ユーザーがいないと仮定
    
    $userRepository->method('save')
                  ->will($this->returnCallback(function($user) {
                      // 保存処理をシミュレート
                      $user->setId(123); // IDを設定
                      return true;
                  }));
    
    // テスト対象のクラスにモックを注入
    $userService = new UserService($userRepository);
    $newUser = $userService->register('new@example.com', 'password123');
    
    $this->assertEquals(123, $newUser->getId());
}

外部APIのモック化:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public function testPaymentProcessing()
{
// 支払いゲートウェイAPIクライアントのモック
$paymentGateway = $this->createMock(PaymentGatewayClient::class);
// 連続した呼び出しに対して異なる応答を設定
$paymentGateway->method('processPayment')
->withConsecutive(
[$this->equalTo(100), $this->anything()],
[$this->equalTo(200), $this->anything()]
)
->willReturnOnConsecutiveCalls(
['status' => 'success', 'transaction_id' => 'tx_123'],
['status' => 'failed', 'error' => 'insufficient_funds']
);
$paymentService = new PaymentService($paymentGateway);
// 1回目の呼び出し(成功)
$result1 = $paymentService->processOrder(['amount' => 100]);
$this->assertTrue($result1->isSuccessful());
// 2回目の呼び出し(失敗)
$result2 = $paymentService->processOrder(['amount' => 200]);
$this->assertFalse($result2->isSuccessful());
}
public function testPaymentProcessing() { // 支払いゲートウェイAPIクライアントのモック $paymentGateway = $this->createMock(PaymentGatewayClient::class); // 連続した呼び出しに対して異なる応答を設定 $paymentGateway->method('processPayment') ->withConsecutive( [$this->equalTo(100), $this->anything()], [$this->equalTo(200), $this->anything()] ) ->willReturnOnConsecutiveCalls( ['status' => 'success', 'transaction_id' => 'tx_123'], ['status' => 'failed', 'error' => 'insufficient_funds'] ); $paymentService = new PaymentService($paymentGateway); // 1回目の呼び出し(成功) $result1 = $paymentService->processOrder(['amount' => 100]); $this->assertTrue($result1->isSuccessful()); // 2回目の呼び出し(失敗) $result2 = $paymentService->processOrder(['amount' => 200]); $this->assertFalse($result2->isSuccessful()); }
public function testPaymentProcessing()
{
    // 支払いゲートウェイAPIクライアントのモック
    $paymentGateway = $this->createMock(PaymentGatewayClient::class);
    
    // 連続した呼び出しに対して異なる応答を設定
    $paymentGateway->method('processPayment')
                  ->withConsecutive(
                      [$this->equalTo(100), $this->anything()],
                      [$this->equalTo(200), $this->anything()]
                  )
                  ->willReturnOnConsecutiveCalls(
                      ['status' => 'success', 'transaction_id' => 'tx_123'],
                      ['status' => 'failed', 'error' => 'insufficient_funds']
                  );
    
    $paymentService = new PaymentService($paymentGateway);
    
    // 1回目の呼び出し(成功)
    $result1 = $paymentService->processOrder(['amount' => 100]);
    $this->assertTrue($result1->isSuccessful());
    
    // 2回目の呼び出し(失敗)
    $result2 = $paymentService->processOrder(['amount' => 200]);
    $this->assertFalse($result2->isSuccessful());
}

モックオブジェクトのふるまいを定義するベストプラクティス

モックを効果的に活用するためのベストプラクティスは以下のとおりです:

ベストプラクティス説明
インターフェースをモック化具体的な実装ではなく、インターフェースに対してモックを作成する
必要最小限のモック化テストに必要なメソッドだけをモック化し、過剰なモックを避ける
アサーションは適切な粒度でモックの詳細すぎる検証は避け、重要な振る舞いのみを検証する
テストのための設計依存性注入を活用し、テスト容易性を考慮した設計にする
一貫したモックの使用同じテストスイート内では一貫したモック戦略を使用する

モックの過剰な使用はテストの保守性を低下させる可能性があるため、適切なバランスを見つけることが重要です。外部依存が少なく、テストが容易な設計を目指しつつ、必要な部分だけをモック化するアプローチが推奨されます。

テストカバレッジの向上 – 網羅性を高める戦略

テストカバレッジは、テストによって実行されたコードの割合を示す指標であり、コード品質の向上と潜在的なバグの発見において重要な役割を果たします。PHPUnitを使ってカバレッジを測定し、戦略的に向上させる方法を解説します。

PHPUnitでカバレッジレポートを生成する方法

PHPUnitでカバレッジレポートを生成するには、まず「Xdebug」または「pcov」PHPエクステンションがインストールされている必要があります。カバレッジレポートは以下のコマンドで生成できます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# テキスト形式のカバレッジレポート(コンソール出力)
./vendor/bin/phpunit --coverage-text
# HTML形式のカバレッジレポート(視覚的なレポート)
./vendor/bin/phpunit --coverage-html ./coverage-report
# XML形式(CIツールとの連携用)
./vendor/bin/phpunit --coverage-xml ./coverage-xml
# 特定のディレクトリやファイルのみカバレッジ測定
./vendor/bin/phpunit --coverage-html ./coverage-report --filter UserTest
# テキスト形式のカバレッジレポート(コンソール出力) ./vendor/bin/phpunit --coverage-text # HTML形式のカバレッジレポート(視覚的なレポート) ./vendor/bin/phpunit --coverage-html ./coverage-report # XML形式(CIツールとの連携用) ./vendor/bin/phpunit --coverage-xml ./coverage-xml # 特定のディレクトリやファイルのみカバレッジ測定 ./vendor/bin/phpunit --coverage-html ./coverage-report --filter UserTest
# テキスト形式のカバレッジレポート(コンソール出力)
./vendor/bin/phpunit --coverage-text

# HTML形式のカバレッジレポート(視覚的なレポート)
./vendor/bin/phpunit --coverage-html ./coverage-report

# XML形式(CIツールとの連携用)
./vendor/bin/phpunit --coverage-xml ./coverage-xml

# 特定のディレクトリやファイルのみカバレッジ測定
./vendor/bin/phpunit --coverage-html ./coverage-report --filter UserTest

HTML形式のレポートは特に有用で、以下の情報が視覚的に確認できます:

  • カバレッジのサマリー(プロジェクト全体の状況)
  • ファイル・クラス・メソッド別のカバレッジ率
  • カバー済み/未カバーの行のハイライト表示
  • 分岐カバレッジの詳細

カバレッジ対象から除外したいコードには、特別なアノテーションを使用できます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// このメソッドはカバレッジ計測から除外される
/** @codeCoverageIgnore */
public function complexLegacyMethod()
{
// ...
}
// 条件付きで除外する例
/** @codeCoverageIgnoreIf PHP_VERSION_ID < 80000 */
public function php8FeatureMethod()
{
// ...
}
// このメソッドはカバレッジ計測から除外される /** @codeCoverageIgnore */ public function complexLegacyMethod() { // ... } // 条件付きで除外する例 /** @codeCoverageIgnoreIf PHP_VERSION_ID < 80000 */ public function php8FeatureMethod() { // ... }
// このメソッドはカバレッジ計測から除外される
/** @codeCoverageIgnore */
public function complexLegacyMethod()
{
    // ...
}

// 条件付きで除外する例
/** @codeCoverageIgnoreIf PHP_VERSION_ID < 80000 */
public function php8FeatureMethod()
{
    // ...
}

条件分岐とエッジケースを確実にテストするアプローチ

高いカバレッジを達成するには、コードのすべての経路(特に条件分岐)をテストする必要があります。効果的なアプローチは以下のとおりです:

1. データプロバイダーを活用した網羅的テスト

データプロバイダーを使って、さまざまな入力パターンを効率的にテストできます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/**
* @dataProvider inputProvider
*/
public function testValidateInput($input, $expectedResult)
{
$validator = new InputValidator();
$this->assertSame($expectedResult, $validator->isValid($input));
}
public function inputProvider()
{
return [
'valid email' => ['test@example.com', true],
'invalid email' => ['invalid-email', false],
'empty string' => ['', false],
'null value' => [null, false],
'numeric input' => [123, false],
'special chars' => ['<script>', false],
];
}
/** * @dataProvider inputProvider */ public function testValidateInput($input, $expectedResult) { $validator = new InputValidator(); $this->assertSame($expectedResult, $validator->isValid($input)); } public function inputProvider() { return [ 'valid email' => ['test@example.com', true], 'invalid email' => ['invalid-email', false], 'empty string' => ['', false], 'null value' => [null, false], 'numeric input' => [123, false], 'special chars' => ['<script>', false], ]; }
/**
 * @dataProvider inputProvider
 */
public function testValidateInput($input, $expectedResult)
{
    $validator = new InputValidator();
    $this->assertSame($expectedResult, $validator->isValid($input));
}

public function inputProvider()
{
    return [
        'valid email' => ['test@example.com', true],
        'invalid email' => ['invalid-email', false],
        'empty string' => ['', false],
        'null value' => [null, false],
        'numeric input' => [123, false],
        'special chars' => ['<script>', false],
    ];
}

2. 境界値テスト

境界値周辺のテストケースを集中的に作成します:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public function testAgeValidation()
{
$validator = new AgeValidator(18, 65); // 18〜65歳が有効範囲
// 境界値テスト
$this->assertFalse($validator->isValid(17)); // 下限未満
$this->assertTrue($validator->isValid(18)); // 下限ちょうど
$this->assertTrue($validator->isValid(19)); // 下限+1
$this->assertTrue($validator->isValid(64)); // 上限-1
$this->assertTrue($validator->isValid(65)); // 上限ちょうど
$this->assertFalse($validator->isValid(66)); // 上限超過
}
public function testAgeValidation() { $validator = new AgeValidator(18, 65); // 18〜65歳が有効範囲 // 境界値テスト $this->assertFalse($validator->isValid(17)); // 下限未満 $this->assertTrue($validator->isValid(18)); // 下限ちょうど $this->assertTrue($validator->isValid(19)); // 下限+1 $this->assertTrue($validator->isValid(64)); // 上限-1 $this->assertTrue($validator->isValid(65)); // 上限ちょうど $this->assertFalse($validator->isValid(66)); // 上限超過 }
public function testAgeValidation()
{
    $validator = new AgeValidator(18, 65); // 18〜65歳が有効範囲
    
    // 境界値テスト
    $this->assertFalse($validator->isValid(17)); // 下限未満
    $this->assertTrue($validator->isValid(18)); // 下限ちょうど
    $this->assertTrue($validator->isValid(19)); // 下限+1
    
    $this->assertTrue($validator->isValid(64)); // 上限-1
    $this->assertTrue($validator->isValid(65)); // 上限ちょうど
    $this->assertFalse($validator->isValid(66)); // 上限超過
}

3. 例外テスト

例外が発生するケースも確実にテストします:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public function testDivisionByZeroThrowsException()
{
$calculator = new Calculator();
$this->expectException(DivisionByZeroException::class);
$calculator->divide(10, 0);
}
public function testDivisionByZeroThrowsException() { $calculator = new Calculator(); $this->expectException(DivisionByZeroException::class); $calculator->divide(10, 0); }
public function testDivisionByZeroThrowsException()
{
    $calculator = new Calculator();
    
    $this->expectException(DivisionByZeroException::class);
    $calculator->divide(10, 0);
}

カバレッジ目標の設定と段階的な改善プロセス

効果的なカバレッジ戦略には、現実的な目標設定と段階的なアプローチが重要です:

  1. 初期目標の設定: まずは60〜70%程度の現実的なカバレッジ目標を設定します
  2. 重要コードの優先: ビジネスロジックや複雑な処理を含むクラスを優先してテストします
  3. CI/CDへの統合: カバレッジレポートを継続的インテグレーションプロセスに組み込みます
  4. 段階的な改善: カバレッジ目標を少しずつ引き上げていきます(例:70%→75%→80%)
  5. リファクタリングとの連携: コード改善時に同時にテストカバレッジも向上させます

カバレッジのみを目標にするのではなく、テストの質も重視することが重要です。100%のカバレッジでも、テストの質が低ければ意味がありません。重要なのは、バグのリスクが高い部分や複雑なロジックに対して堅牢なテストを作成することです。

実践的なPHPUnitテスト事例 – 現場で使えるパターン

PHPUnitの基本を理解したら、次は実際の開発現場で使われている実践的なテストパターンを見ていきましょう。主要フレームワークでの具体的な実装例や、レガシーコードへのテスト導入アプローチなど、現場ですぐに活用できるノウハウを解説します。

Laravelプロジェクトでのモデルとコントローラーのテスト例

Laravelでは、PHPUnitが標準で統合されており、豊富なテスト支援機能を利用できます。

モデルのテスト

Laravelモデルのテストでは、リレーションシップ、スコープ、ミューテータなどの機能を検証します:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
// UserModelTest.php
namespace Tests\Unit;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserModelTest extends TestCase
{
// テスト間でデータベースをリセット
use RefreshDatabase;
public function testUserHasManyPosts()
{
// ユーザーと投稿を作成
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
// リレーションシップが正しく動作することを確認
$this->assertTrue($user->posts->contains($post));
$this->assertEquals(1, $user->posts->count());
}
public function testActiveScope()
{
// アクティブユーザーと非アクティブユーザーを作成
$activeUser = User::factory()->create(['active' => true]);
$inactiveUser = User::factory()->create(['active' => false]);
// activeスコープが正しく動作することを確認
$activeUsers = User::active()->get();
$this->assertTrue($activeUsers->contains($activeUser));
$this->assertFalse($activeUsers->contains($inactiveUser));
}
public function testFullNameAccessor()
{
$user = User::factory()->create([
'first_name' => 'John',
'last_name' => 'Doe'
]);
// アクセサが正しく動作することを確認
$this->assertEquals('John Doe', $user->full_name);
}
}
<?php // UserModelTest.php namespace Tests\Unit; use App\Models\User; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class UserModelTest extends TestCase { // テスト間でデータベースをリセット use RefreshDatabase; public function testUserHasManyPosts() { // ユーザーと投稿を作成 $user = User::factory()->create(); $post = Post::factory()->create(['user_id' => $user->id]); // リレーションシップが正しく動作することを確認 $this->assertTrue($user->posts->contains($post)); $this->assertEquals(1, $user->posts->count()); } public function testActiveScope() { // アクティブユーザーと非アクティブユーザーを作成 $activeUser = User::factory()->create(['active' => true]); $inactiveUser = User::factory()->create(['active' => false]); // activeスコープが正しく動作することを確認 $activeUsers = User::active()->get(); $this->assertTrue($activeUsers->contains($activeUser)); $this->assertFalse($activeUsers->contains($inactiveUser)); } public function testFullNameAccessor() { $user = User::factory()->create([ 'first_name' => 'John', 'last_name' => 'Doe' ]); // アクセサが正しく動作することを確認 $this->assertEquals('John Doe', $user->full_name); } }
<?php
// UserModelTest.php
namespace Tests\Unit;

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

class UserModelTest extends TestCase
{
    // テスト間でデータベースをリセット
    use RefreshDatabase;
    
    public function testUserHasManyPosts()
    {
        // ユーザーと投稿を作成
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);
        
        // リレーションシップが正しく動作することを確認
        $this->assertTrue($user->posts->contains($post));
        $this->assertEquals(1, $user->posts->count());
    }
    
    public function testActiveScope()
    {
        // アクティブユーザーと非アクティブユーザーを作成
        $activeUser = User::factory()->create(['active' => true]);
        $inactiveUser = User::factory()->create(['active' => false]);
        
        // activeスコープが正しく動作することを確認
        $activeUsers = User::active()->get();
        $this->assertTrue($activeUsers->contains($activeUser));
        $this->assertFalse($activeUsers->contains($inactiveUser));
    }
    
    public function testFullNameAccessor()
    {
        $user = User::factory()->create([
            'first_name' => 'John',
            'last_name' => 'Doe'
        ]);
        
        // アクセサが正しく動作することを確認
        $this->assertEquals('John Doe', $user->full_name);
    }
}

コントローラーのテスト

Laravelコントローラーのテストでは、HTTPリクエスト、レスポンス、リダイレクト、ビュー変数などを検証します:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
// PostControllerTest.php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostControllerTest extends TestCase
{
use RefreshDatabase;
public function testIndex()
{
// 複数の投稿を作成
$posts = Post::factory()->count(3)->create();
// GETリクエストを送信
$response = $this->get('/posts');
// レスポンスの検証
$response->assertStatus(200);
$response->assertViewIs('posts.index');
$response->assertViewHas('posts');
// すべての投稿がビューに渡されていることを確認
$viewPosts = $response->viewData('posts');
$this->assertEquals($posts->count(), $viewPosts->count());
}
public function testStore()
{
// ユーザーを認証状態にする
$user = User::factory()->create();
$this->actingAs($user);
// 新しい投稿データ
$postData = [
'title' => 'Test Post',
'content' => 'This is a test post content.'
];
// POSTリクエストを送信
$response = $this->post('/posts', $postData);
// データベースに保存されたことを確認
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'user_id' => $user->id
]);
// リダイレクトの確認
$response->assertRedirect('/posts');
$response->assertSessionHas('success');
}
}
<?php // PostControllerTest.php namespace Tests\Feature; use App\Models\User; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostControllerTest extends TestCase { use RefreshDatabase; public function testIndex() { // 複数の投稿を作成 $posts = Post::factory()->count(3)->create(); // GETリクエストを送信 $response = $this->get('/posts'); // レスポンスの検証 $response->assertStatus(200); $response->assertViewIs('posts.index'); $response->assertViewHas('posts'); // すべての投稿がビューに渡されていることを確認 $viewPosts = $response->viewData('posts'); $this->assertEquals($posts->count(), $viewPosts->count()); } public function testStore() { // ユーザーを認証状態にする $user = User::factory()->create(); $this->actingAs($user); // 新しい投稿データ $postData = [ 'title' => 'Test Post', 'content' => 'This is a test post content.' ]; // POSTリクエストを送信 $response = $this->post('/posts', $postData); // データベースに保存されたことを確認 $this->assertDatabaseHas('posts', [ 'title' => 'Test Post', 'user_id' => $user->id ]); // リダイレクトの確認 $response->assertRedirect('/posts'); $response->assertSessionHas('success'); } }
<?php
// PostControllerTest.php
namespace Tests\Feature;

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

class PostControllerTest extends TestCase
{
    use RefreshDatabase;
    
    public function testIndex()
    {
        // 複数の投稿を作成
        $posts = Post::factory()->count(3)->create();
        
        // GETリクエストを送信
        $response = $this->get('/posts');
        
        // レスポンスの検証
        $response->assertStatus(200);
        $response->assertViewIs('posts.index');
        $response->assertViewHas('posts');
        
        // すべての投稿がビューに渡されていることを確認
        $viewPosts = $response->viewData('posts');
        $this->assertEquals($posts->count(), $viewPosts->count());
    }
    
    public function testStore()
    {
        // ユーザーを認証状態にする
        $user = User::factory()->create();
        $this->actingAs($user);
        
        // 新しい投稿データ
        $postData = [
            'title' => 'Test Post',
            'content' => 'This is a test post content.'
        ];
        
        // POSTリクエストを送信
        $response = $this->post('/posts', $postData);
        
        // データベースに保存されたことを確認
        $this->assertDatabaseHas('posts', [
            'title' => 'Test Post',
            'user_id' => $user->id
        ]);
        
        // リダイレクトの確認
        $response->assertRedirect('/posts');
        $response->assertSessionHas('success');
    }
}

Symfonyアプリケーションでのサービスクラステスト実装

Symfonyでは、サービスコンテナを活用したテストが一般的です。以下はサービスクラスのテスト例です:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
// src/Tests/Service/NewsletterServiceTest.php
namespace App\Tests\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\EmailService;
use App\Service\NewsletterService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class NewsletterServiceTest extends KernelTestCase
{
public function testSendNewsletter()
{
self::bootKernel();
$container = static::getContainer();
// モックリポジトリを作成
$userRepository = $this->createMock(UserRepository::class);
$userRepository->expects($this->once())
->method('findSubscribedUsers')
->willReturn([
(new User())->setEmail('user1@example.com'),
(new User())->setEmail('user2@example.com')
]);
// モックメールサービスを作成
$emailService = $this->createMock(EmailService::class);
$emailService->expects($this->exactly(2))
->method('sendEmail');
// テスト対象のサービスにモックを注入
$newsletterService = new NewsletterService($userRepository, $emailService);
// ニュースレター送信をテスト
$result = $newsletterService->sendNewsletter('New Features', 'Check out our latest features!');
$this->assertEquals(2, $result['sent']);
$this->assertEquals(0, $result['failed']);
}
}
<?php // src/Tests/Service/NewsletterServiceTest.php namespace App\Tests\Service; use App\Entity\User; use App\Repository\UserRepository; use App\Service\EmailService; use App\Service\NewsletterService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class NewsletterServiceTest extends KernelTestCase { public function testSendNewsletter() { self::bootKernel(); $container = static::getContainer(); // モックリポジトリを作成 $userRepository = $this->createMock(UserRepository::class); $userRepository->expects($this->once()) ->method('findSubscribedUsers') ->willReturn([ (new User())->setEmail('user1@example.com'), (new User())->setEmail('user2@example.com') ]); // モックメールサービスを作成 $emailService = $this->createMock(EmailService::class); $emailService->expects($this->exactly(2)) ->method('sendEmail'); // テスト対象のサービスにモックを注入 $newsletterService = new NewsletterService($userRepository, $emailService); // ニュースレター送信をテスト $result = $newsletterService->sendNewsletter('New Features', 'Check out our latest features!'); $this->assertEquals(2, $result['sent']); $this->assertEquals(0, $result['failed']); } }
<?php
// src/Tests/Service/NewsletterServiceTest.php
namespace App\Tests\Service;

use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\EmailService;
use App\Service\NewsletterService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class NewsletterServiceTest extends KernelTestCase
{
    public function testSendNewsletter()
    {
        self::bootKernel();
        $container = static::getContainer();
        
        // モックリポジトリを作成
        $userRepository = $this->createMock(UserRepository::class);
        $userRepository->expects($this->once())
                       ->method('findSubscribedUsers')
                       ->willReturn([
                           (new User())->setEmail('user1@example.com'),
                           (new User())->setEmail('user2@example.com')
                       ]);
        
        // モックメールサービスを作成
        $emailService = $this->createMock(EmailService::class);
        $emailService->expects($this->exactly(2))
                    ->method('sendEmail');
        
        // テスト対象のサービスにモックを注入
        $newsletterService = new NewsletterService($userRepository, $emailService);
        
        // ニュースレター送信をテスト
        $result = $newsletterService->sendNewsletter('New Features', 'Check out our latest features!');
        
        $this->assertEquals(2, $result['sent']);
        $this->assertEquals(0, $result['failed']);
    }
}

レガシーコードへのテスト導入アプローチ

テストがないレガシーコードに対してテストを導入する場合、段階的なアプローチが効果的です:

  1. 特性テスト(Characterization Tests): まず現在の動作を把握するためのテストを書きます。これは「動作をドキュメント化するテスト」で、現在の出力が正しいかどうかは考慮しません。
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 既存の計算ロジックの動作を把握するテスト
public function testLegacyCalculationLogic()
{
$calculator = new LegacyCalculator();
// 現在の動作を記録するテスト
$this->assertEquals(150, $calculator->calculateTotal(100, 0.5, 0));
$this->assertEquals(212, $calculator->calculateTotal(200, 0.1, 2));
// エッジケースも記録
$this->assertEquals(0, $calculator->calculateTotal(0, 0.5, 0));
$this->assertEquals(105, $calculator->calculateTotal(-100, 0.5, 0));
}
// 既存の計算ロジックの動作を把握するテスト public function testLegacyCalculationLogic() { $calculator = new LegacyCalculator(); // 現在の動作を記録するテスト $this->assertEquals(150, $calculator->calculateTotal(100, 0.5, 0)); $this->assertEquals(212, $calculator->calculateTotal(200, 0.1, 2)); // エッジケースも記録 $this->assertEquals(0, $calculator->calculateTotal(0, 0.5, 0)); $this->assertEquals(105, $calculator->calculateTotal(-100, 0.5, 0)); }
// 既存の計算ロジックの動作を把握するテスト
public function testLegacyCalculationLogic()
{
    $calculator = new LegacyCalculator();
    
    // 現在の動作を記録するテスト
    $this->assertEquals(150, $calculator->calculateTotal(100, 0.5, 0));
    $this->assertEquals(212, $calculator->calculateTotal(200, 0.1, 2));
    // エッジケースも記録
    $this->assertEquals(0, $calculator->calculateTotal(0, 0.5, 0));
    $this->assertEquals(105, $calculator->calculateTotal(-100, 0.5, 0));
}
  1. 安全地帯の確保: テストで保護されたコードから徐々にリファクタリングを行います:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// リファクタリング後のコードをテスト
public function testRefactoredCalculationLogic()
{
$legacyCalculator = new LegacyCalculator();
$newCalculator = new ImprovedCalculator();
// 様々な入力に対して同じ結果が得られることを確認
$testCases = [
[100, 0.5, 0],
[200, 0.1, 2],
[0, 0.5, 0],
[-100, 0.5, 0]
];
foreach ($testCases as $case) {
$legacyResult = $legacyCalculator->calculateTotal(...$case);
$newResult = $newCalculator->calculateTotal(...$case);
$this->assertEquals($legacyResult, $newResult);
}
}
// リファクタリング後のコードをテスト public function testRefactoredCalculationLogic() { $legacyCalculator = new LegacyCalculator(); $newCalculator = new ImprovedCalculator(); // 様々な入力に対して同じ結果が得られることを確認 $testCases = [ [100, 0.5, 0], [200, 0.1, 2], [0, 0.5, 0], [-100, 0.5, 0] ]; foreach ($testCases as $case) { $legacyResult = $legacyCalculator->calculateTotal(...$case); $newResult = $newCalculator->calculateTotal(...$case); $this->assertEquals($legacyResult, $newResult); } }
// リファクタリング後のコードをテスト
public function testRefactoredCalculationLogic()
{
    $legacyCalculator = new LegacyCalculator();
    $newCalculator = new ImprovedCalculator();
    
    // 様々な入力に対して同じ結果が得られることを確認
    $testCases = [
        [100, 0.5, 0],
        [200, 0.1, 2],
        [0, 0.5, 0],
        [-100, 0.5, 0]
    ];
    
    foreach ($testCases as $case) {
        $legacyResult = $legacyCalculator->calculateTotal(...$case);
        $newResult = $newCalculator->calculateTotal(...$case);
        $this->assertEquals($legacyResult, $newResult);
    }
}
  1. クラス抽出とインターフェース導入: レガシーコードを少しずつ分解し、テスト可能な構造に改善します。

レガシーコードへのテスト導入は一朝一夕にはいきませんが、着実に進めることで技術的負債を減らし、コードの品質向上につながります。

これらの実践的な例は、様々なシナリオでPHPUnitを効果的に活用するための基盤となります。フレームワークの特性を理解し、状況に応じた適切なテスト戦略を選択することで、堅牢なテストスイートを構築できます。

CI/CDパイプラインへのPHPUnit統合ガイド

PHPUnitをCI/CDパイプラインに統合することで、コードの品質を継続的に監視し、問題を早期に発見することができます。ここでは、主要なCI/CDツールでのPHPUnit統合方法を解説します。

GitHubActionsでPHPUnitテストを自動化する設定例

GitHub Actionsは、GitHubに統合されたCI/CDプラットフォームであり、PHPUnitテストを簡単に自動化できます。以下は基本的な設定ファイル例です:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# .github/workflows/tests.yml
name: PHP Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
# 複数のPHPバージョンでテストを実行
strategy:
matrix:
php-versions: ['8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v3
# PHP環境のセットアップ
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, intl, pdo_mysql
coverage: xdebug
# Composerキャッシュの設定
- name: Cache Composer packages
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
# PHPUnitでテスト実行
- name: Run tests
run: vendor/bin/phpunit --coverage-clover=coverage.xml
# コードカバレッジの送信(例:Codecov)
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
# .github/workflows/tests.yml name: PHP Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest # 複数のPHPバージョンでテストを実行 strategy: matrix: php-versions: ['8.0', '8.1', '8.2'] steps: - uses: actions/checkout@v3 # PHP環境のセットアップ - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl, pdo_mysql coverage: xdebug # Composerキャッシュの設定 - name: Cache Composer packages 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 # PHPUnitでテスト実行 - name: Run tests run: vendor/bin/phpunit --coverage-clover=coverage.xml # コードカバレッジの送信(例:Codecov) - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml fail_ci_if_error: true
# .github/workflows/tests.yml
name: PHP Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    # 複数のPHPバージョンでテストを実行
    strategy:
      matrix:
        php-versions: ['8.0', '8.1', '8.2']
    
    steps:
    - uses: actions/checkout@v3
    
    # PHP環境のセットアップ
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php-versions }}
        extensions: mbstring, intl, pdo_mysql
        coverage: xdebug
    
    # Composerキャッシュの設定
    - name: Cache Composer packages
      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
    
    # PHPUnitでテスト実行
    - name: Run tests
      run: vendor/bin/phpunit --coverage-clover=coverage.xml
    
    # コードカバレッジの送信(例:Codecov)
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml
        fail_ci_if_error: true

この設定により、プッシュやプルリクエスト時に自動的に複数のPHPバージョンでテストが実行され、コードカバレッジ情報が収集されます。

JenkinsでのPHPUnitテスト実行とレポート生成の方法

Jenkinsを使用したPHPUnitテストの自動化では、Jenkinsfileを使用して設定できます:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Jenkinsfile
pipeline {
agent any
stages {
stage('Prepare') {
steps {
sh 'composer install --no-interaction --no-progress'
}
}
stage('Test') {
steps {
sh 'vendor/bin/phpunit --log-junit test-reports/junit.xml --coverage-html test-reports/coverage'
}
post {
always {
// JUnitテストレポートの公開
junit 'test-reports/junit.xml'
// コードカバレッジレポートの公開
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'test-reports/coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
stage('Deploy') {
when {
expression { currentBuild.resultIsBetterOrEqualTo('SUCCESS') }
}
steps {
echo 'Deployment steps would go here'
}
}
}
post {
failure {
// テスト失敗時の通知
emailext (
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check the build logs at ${env.BUILD_URL}",
to: 'team@example.com'
)
// Slack通知の例
slackSend channel: '#alerts', color: 'danger', message: "テスト失敗: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
}
}
// Jenkinsfile pipeline { agent any stages { stage('Prepare') { steps { sh 'composer install --no-interaction --no-progress' } } stage('Test') { steps { sh 'vendor/bin/phpunit --log-junit test-reports/junit.xml --coverage-html test-reports/coverage' } post { always { // JUnitテストレポートの公開 junit 'test-reports/junit.xml' // コードカバレッジレポートの公開 publishHTML(target: [ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'test-reports/coverage', reportFiles: 'index.html', reportName: 'Coverage Report' ]) } } } stage('Deploy') { when { expression { currentBuild.resultIsBetterOrEqualTo('SUCCESS') } } steps { echo 'Deployment steps would go here' } } } post { failure { // テスト失敗時の通知 emailext ( subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}", body: "Check the build logs at ${env.BUILD_URL}", to: 'team@example.com' ) // Slack通知の例 slackSend channel: '#alerts', color: 'danger', message: "テスト失敗: ${env.JOB_NAME} #${env.BUILD_NUMBER}" } } }
// Jenkinsfile
pipeline {
    agent any
    
    stages {
        stage('Prepare') {
            steps {
                sh 'composer install --no-interaction --no-progress'
            }
        }
        
        stage('Test') {
            steps {
                sh 'vendor/bin/phpunit --log-junit test-reports/junit.xml --coverage-html test-reports/coverage'
            }
            post {
                always {
                    // JUnitテストレポートの公開
                    junit 'test-reports/junit.xml'
                    
                    // コードカバレッジレポートの公開
                    publishHTML(target: [
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'test-reports/coverage',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }
        
        stage('Deploy') {
            when {
                expression { currentBuild.resultIsBetterOrEqualTo('SUCCESS') }
            }
            steps {
                echo 'Deployment steps would go here'
            }
        }
    }
    
    post {
        failure {
            // テスト失敗時の通知
            emailext (
                subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Check the build logs at ${env.BUILD_URL}",
                to: 'team@example.com'
            )
            
            // Slack通知の例
            slackSend channel: '#alerts', color: 'danger', message: "テスト失敗: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

このJenkinsfileでは、テスト実行、結果レポートの生成、テスト失敗時の通知が設定されています。

テスト失敗時のアラートと対応フローの構築

テスト失敗時のアラートと対応フローは、効率的なCI/CDパイプラインの重要な要素です:

  1. 通知システムの構築:
    • Slack/Teams/Discordへの通知
    • メール通知(担当者またはチーム全体へ)
    • プルリクエストへのコメント自動投稿
  2. アラート情報の充実:
    • 失敗したテストの詳細情報
    • 関連するコミット情報とコミッター
    • テスト失敗の影響範囲
    • 問題解決のための推奨アクション
  3. 対応フローの自動化:
    • テスト失敗のチケット自動作成(Jira、GitHubなど)
    • 特定のテスト失敗パターンに基づく自動対応
    • 緊急度に応じた通知レベルの調整

例えば、GitHubActionsでのSlack通知の設定:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
- name: Slack Notification on Failure
uses: 8398a7/action-slack@v3
if: failure()
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,workflow,job
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Slack Notification on Failure uses: 8398a7/action-slack@v3 if: failure() with: status: ${{ job.status }} fields: repo,message,commit,author,action,workflow,job env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Slack Notification on Failure
  uses: 8398a7/action-slack@v3
  if: failure()
  with:
    status: ${{ job.status }}
    fields: repo,message,commit,author,action,workflow,job
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

CI/CDパイプラインへのPHPUnit統合により、開発チームは迅速かつ安全にコードを提供できるようになります。自動テストを通じた早期のフィードバックループは、バグの早期発見と修正を促進し、全体的なコード品質の向上に貢献します。

PHPUnitテスト文化をチームに根付かせるためのステップ

技術的な側面だけでなく、チーム全体にテスト文化を浸透させることもPHPUnitを効果的に活用するための重要な要素です。ここでは、PHPUnitテスト文化をチームに根付かせるための実践的なステップを紹介します。

コードレビューでテストコードの品質を高める方法

コードレビューは、テストコードの品質向上とベストプラクティスの共有に最適な機会です:

  1. テストレビューチェックリストの作成
    • テストの命名規則は明確か
    • テストは独立して実行可能か(他のテストに依存していないか)
    • アサーションは具体的で明確か
    • 過剰なモック化を避けているか
    • カバレッジは適切か(条件分岐をすべてカバーしているか)
  2. レビュー文化の確立
    • 「テストなしでは機能は完成とみなさない」という基準を設ける
    • ビジネスロジックの変更には必ずテストの追加または更新を求める
    • 良いテストコードの例を共有し、称賛する

具体的なレビューコメント例:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
「このControllerテストでは、認証失敗のケースもテストするとよいでしょう」
「このテストではモックの設定が複雑すぎます。テスト対象を小さくすることを検討しませんか?」
「この条件分岐のfalseケースもテストに含めると、カバレッジが向上します」
「このControllerテストでは、認証失敗のケースもテストするとよいでしょう」 「このテストではモックの設定が複雑すぎます。テスト対象を小さくすることを検討しませんか?」 「この条件分岐のfalseケースもテストに含めると、カバレッジが向上します」
「このControllerテストでは、認証失敗のケースもテストするとよいでしょう」
「このテストではモックの設定が複雑すぎます。テスト対象を小さくすることを検討しませんか?」
「この条件分岐のfalseケースもテストに含めると、カバレッジが向上します」

ペアプログラミングによるテスト作成スキルの共有

ペアプログラミングは、テストスキルを効果的に共有する強力な手法です:

  • 経験者と未経験者のペアリング: テスト経験が豊富なメンバーと経験の少ないメンバーをペアにする
  • ローテーション: 定期的にペアを変更し、チーム全体でテスト知識を均等に広げる
  • テストファースト・セッション: 「テストから書く」ことに特化したペアプログラミングセッションを実施

ペアプログラミングの効果:

  • 暗黙知の共有(ショートカット、ツールの使い方など)
  • リアルタイムでのフィードバック
  • テストに対する心理的障壁の低減

テスト駆動の開発サイクルをチームに定着させるコツ

テスト駆動開発(TDD)をチームに定着させるには、段階的なアプローチが効果的です:

  1. 小さく始める
    • 新規機能や小さなバグ修正からTDDを適用
    • 全コードベースでなく、特定のモジュールやサービスに限定して導入
  2. 共同学習の促進
    • 週1回の「コーディングDojo」: チーム全員で同じ課題をTDDで解く
    • 「モブプログラミング」: チーム全員で一つの問題をTDDで解決
    • 「カタ」練習: 定型的な問題をTDDで繰り返し解く
  3. 可視化と祝福
    • テストカバレッジやテスト数の増加を視覚化して共有
    • TDDの成功事例(バグの早期発見、安全なリファクタリングなど)を共有
    • 「テストの勇者」など、テスト文化推進に貢献したメンバーを称える
  4. 実践的なワークショップ
    • PHPUnitの基本から応用までを学ぶハンズオンセッション
    • レガシーコードへのテスト導入ワークショップ
    • 「テストしやすい設計」についての勉強会

テスト文化は一朝一夕には根付きませんが、小さな成功体験を積み重ね、チーム全体で価値を共有することで、徐々に「テストはコードの一部」という認識を浸透させることができます。最終的には、テストを書くことが特別なタスクではなく、コーディングの自然な一部となり、チームの生産性と品質が大きく向上します。