第15章 PHPUnit の拡張

テストを書きやすくする、あるいはテストの実行結果の表示方法を変更するなど、 PHPUnit はさまざまな方法で拡張することができます。 PHPUnit を拡張するための第一歩をここで説明します。

PHPUnit_Framework_TestCase のサブクラスの作成

PHPUnit_Framework_TestCase を継承した抽象サブクラスにカスタムアサーションやユーティリティメソッドを書き、 そのクラスをさらに継承してテストクラスを作成します。 これが、PHPUnit を拡張するための一番簡単な方法です。

カスタムアサーションの作成

カスタムアサーションを作成するときには、PHPUnit 自体のアサーションの実装方法を真似るのがおすすめです。 例 15.1 を見ればわかるとおり、 assertTrue() メソッドは isTrue() および assertThat() メソッドの単なるラッパーに過ぎません。 isTrue() が matcher オブジェクトを作り、それを assertThat() に渡して評価しています。

例 15.1: PHPUnit_Framework_Assert クラスの assertTrue() および isTrue() メソッド

<?php
abstract class PHPUnit_Framework_Assert
{
    // ...

    /**
     * Asserts that a condition is true.
     *
     * @param  boolean $condition
     * @param  string  $message
     * @throws PHPUnit_Framework_AssertionFailedError
     */
    public static function assertTrue($condition, $message = '')
    {
        self::assertThat($condition, self::isTrue(), $message);
    }

    // ...

    /**
     * Returns a PHPUnit_Framework_Constraint_IsTrue matcher object.
     *
     * @return PHPUnit_Framework_Constraint_IsTrue
     * @since  Method available since Release 3.3.0
     */
    public static function isTrue()
    {
        return new PHPUnit_Framework_Constraint_IsTrue;
    }

    // ...
}?>


例 15.2 は、 PHPUnit_Framework_Constraint_IsTrue が matcher オブジェクト (あるいは制約) のために抽象クラス PHPUnit_Framework_Constraint を継承している部分です。

例 15.2: PHPUnit_Framework_Constraint_IsTrue クラス

<?php
class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
    /**
     * Evaluates the constraint for parameter $other. Returns TRUE if the
     * constraint is met, FALSE otherwise.
     *
     * @param mixed $other Value or object to evaluate.
     * @return bool
     */
    public function matches($other)
    {
        return $other === TRUE;
    }

    /**
     * Returns a string representation of the constraint.
     *
     * @return string
     */
    public function toString()
    {
        return 'is true';
    }
}?>


assertTrue()isTrue() メソッドの実装を PHPUnit_Framework_Constraint_IsTrue クラスと同じようにしておけば、 アサーションの評価やタスクの記録 (テストの統計情報に自動的に更新するなど) を assertThat() が自動的に行ってくれるようになります。 さらに、モックオブジェクトを設定する際の matcher として isTrue() メソッドを使えるようにもなります。

PHPUnit_Framework_TestListener の実装

例 15.3 は、 PHPUnit_Framework_TestListener インターフェイスのシンプルな実装例です。

例 15.3: シンプルなテストリスナー

<?php
class SimpleTestListener implements PHPUnit_Framework_TestListener
{
    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' の実行中にエラーが発生\n", $test->getName());
    }

    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
    {
        printf("テスト '%s' に失敗\n", $test->getName());
    }

    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' は未完成\n", $test->getName());
    }

    public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' は危険\n", $test->getName());
    }

    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' をスキップ\n", $test->getName());
    }

    public function startTest(PHPUnit_Framework_Test $test)
    {
        printf("テスト '%s' が開始\n", $test->getName());
    }

    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("テスト '%s' が終了\n", $test->getName());
    }

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("テストスイート '%s' が開始\n", $suite->getName());
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("テストスイート '%s' が終了\n", $suite->getName());
    }
}
?>


例 15.4 は、抽象クラス PHPUnit_Framework_BaseTestListener のサブクラスを作る例です。これは、インターフェイスのメソッドのうち実際に使うものだけを指定し、 他のメソッドについては空の実装を提供します。

例 15.4: ベーステストリスナーの利用法

<?php
class ShortTestListener extends PHPUnit_Framework_BaseTestListener
{
    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("テスト '%s' が終了\n", $test->getName());
    }
}
?>


「テストリスナー」 に、自作のテストリスナーをテスト実行時にアタッチするための PHPUnit の設定方法についての説明があります。

PHPUnit_Extensions_TestDecorator のサブクラスの作成

PHPUnit_Extensions_TestDecorator のサブクラスでテストケースあるいはテストスイートをラッピングし、 デコレータパターンを使用することで 各テストの実行前後に何らかの処理をさせることができます。

PHPUnit には、PHPUnit_Extensions_RepeatedTest および PHPUnit_Extensions_TestSetup という 2 つの具象テストデコレータが付属しています。 前者はテストを繰り返し実行し、それらが全て成功した場合にのみ成功とみなします。 後者については 第 4 章 で説明しました。

例 15.5 は、テストデコレータ PHPUnit_Extensions_RepeatedTest の一部を抜粋したものです。独自のデコレータを作成するための参考にしてください。

例 15.5: RepeatedTest デコレータ

<?php
require_once 'PHPUnit/Extensions/TestDecorator.php';

class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
    private $timesRepeat = 1;

    public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
    {
        parent::__construct($test);

        if (is_integer($timesRepeat) &&
            $timesRepeat >= 0) {
            $this->timesRepeat = $timesRepeat;
        }
    }

    public function count()
    {
        return $this->timesRepeat * $this->test->count();
    }

    public function run(PHPUnit_Framework_TestResult $result = NULL)
    {
        if ($result === NULL) {
            $result = $this->createResult();
        }

        for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
            $this->test->run($result);
        }

        return $result;
    }
}
?>


PHPUnit_Framework_Test の実装

PHPUnit_Framework_Test インターフェイスの機能は限られており、 実装するのは簡単です。PHPUnit_Framework_Test を実装するのは PHPUnit_Framework_TestCase の実装より単純で、 これを用いて例えば データ駆動のテスト (data-driven tests) などを実行します。

カンマ区切り (CSV) ファイルの値と比較する、データ駆動のテストを 例 15.6 に示します。このファイルの各行は foo;bar のような形式になっており (訳注: CSV じゃない……)、 最初の値が期待値で 2 番目の値が実際の値です。

例 15.6: データ駆動のテスト

<?php
class DataDrivenTest implements PHPUnit_Framework_Test
{
    private $lines;

    public function __construct($dataFile)
    {
        $this->lines = file($dataFile);
    }

    public function count()
    {
        return 1;
    }

    public function run(PHPUnit_Framework_TestResult $result = NULL)
    {
        if ($result === NULL) {
            $result = new PHPUnit_Framework_TestResult;
        }

        foreach ($this->lines as $line) {
            $result->startTest($this);
            PHP_Timer::start();
            $stopTime = NULL;

            list($expected, $actual) = explode(';', $line);

            try {
                PHPUnit_Framework_Assert::assertEquals(
                  trim($expected), trim($actual)
                );
            }

            catch (PHPUnit_Framework_AssertionFailedError $e) {
                $stopTime = PHP_Timer::stop();
                $result->addFailure($this, $e, $stopTime);
            }

            catch (Exception $e) {
                $stopTime = PHP_Timer::stop();
                $result->addError($this, $e, $stopTime);
            }

            if ($stopTime === NULL) {
                $stopTime = PHP_Timer::stop();
            }

            $result->endTest($this, $stopTime);
        }

        return $result;
    }
}

$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 4.2.0 by Sebastian Bergmann.

.F

Time: 0 seconds

There was 1 failure:

1) DataDrivenTest
Failed asserting that two strings are equal.
expected string <bar>
difference      <  x>
got string      <baz>
/home/sb/DataDrivenTest.php:32
/home/sb/DataDrivenTest.php:53

FAILURES!
Tests: 2, Failures: 1.