第 15 章 扩展 PHPUnit

可以用多种方式对 PHPUnit 进行扩展,使编写测试更容易,以及对运行测试所得到的反馈进行定制。扩展 PHPUnit 时,一般从这些点入手:

从 PHPUnit_Framework_TestCase 派生子类

将自定义的断言和工具方法写在 PHPUnit_Framework_TestCase 的一个抽象子类中,然后从这个抽象子类派生你的测试用例类。这是扩展 PHPUnit 的最容易的方法。

编写自定义断言

编写自定义断言时,最佳实践是遵循 PHPUnit 自有断言的实现方式。正如例 15.1中所示,assertTrue() 方法只是对 isTrue()assertThat() 方法的外包覆:isTrue() 创建了一个匹配器对象,将其传递给 assertThat() 进行评定。

例 15.1: PHPUnit_Framework_Assert 类的 assertTrue() 与 isTrue() 方法

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

    /**
     * 断言某个条件为真。
     *
     * @param  boolean $condition
     * @param  string  $message
     * @throws PHPUnit_Framework_AssertionFailedError
     */
    public static function assertTrue($condition, $message = '')
    {
        self::assertThat($condition, self::isTrue(), $message);
    }

    // ...

    /**
     * 返回一个 PHPUnit_Framework_Constraint_IsTrue 匹配器对象
     *
     * @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 是如何扩展针对匹配器对象(或约束)的抽象基类 PHPUnit_Framework_Constraint的。

例 15.2: PHPUnit_Framework_Constraint_IsTrue 类

<?php
class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
    /**
     * 对参数 $other 进行约束评定。如果符合约束,
     * 返回 TRUE,否则返回 FALSE。
     *
     * @param mixed $other Value or object to evaluate.
     * @return bool
     */
    public function matches($other)
    {
        return $other === TRUE;
    }

    /**
     * 返回代表此约束的字符串。
     *
     * @return string
     */
    public function toString()
    {
        return 'is true';
    }
}?>


在实现 assertTrue()isTrue() 方法及 PHPUnit_Framework_Constraint_IsTrue 类时所付出的努力带来了一些好处,assertThat() 能够自动负责起断言的评定与任务簿记(例如为了统计目的而对其进行计数)工作。此外, 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("Error while running test '%s'.\n", $test->getName());
    }

    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
    {
        printf("Test '%s' failed.\n", $test->getName());
    }

    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' is incomplete.\n", $test->getName());
    }

    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' has been skipped.\n", $test->getName());
    }

    public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' is deemed risky.\n", $test->getName());
    }

    public function startTest(PHPUnit_Framework_Test $test)
    {
        printf("Test '%s' started.\n", $test->getName());
    }

    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("Test '%s' ended.\n", $test->getName());
    }

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("TestSuite '%s' started.\n", $suite->getName());
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("TestSuite '%s' ended.\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("Test '%s' ended.\n", $test->getName());
    }
}
?>


“测试监听器(Test Listeners)”一节中可以看到如何配置 PHPUnit 来将你的测试监听器接入到测试执行过程中。

从 PHPUnit_Extensions_TestDecorator 派生子类

可以将测试用例或者测试套件包装在 PHPUnit_Extensions_TestDecorator 的子类中并运用 Decorator(修饰器)设计模式来在测试运行前后执行一些动作。

PHPUnit 了包含了两个具体的测试修饰器: PHPUnit_Extensions_RepeatedTestPHPUnit_Extensions_TestSetup。前者用于重复运行某个测试,并且只在全部次数都成功时计为成功。后者则在 第 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 的实现来运行数据驱动测试

例 15.6展示了一个数据驱动的测试用例类,对来自 CSV 文件内的值进行比较。这个文件内的每个行看起来类似于 foo;bar,第一个值是期望值,第二个值则是实际值。

例 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.