PHPUnit的监听器(Listener)与扩展:实现测试结果的自定义报告与集成

PHPUnit的监听器(Listener)与扩展:实现测试结果的自定义报告与集成

大家好!今天我们来深入探讨PHPUnit的监听器(Listener)与扩展,它们是PHPUnit框架中非常强大的特性,允许我们自定义测试报告、集成外部工具,以及在测试生命周期的各个阶段执行自定义逻辑。

1. 监听器(Listener)的概念

PHPUnit的监听器是一个类,它实现了PHPUnitFrameworkTestListener接口。这个接口定义了一系列方法,这些方法会在测试运行的不同阶段被PHPUnit调用。通过实现这些方法,我们可以监听测试的执行过程,并在特定的事件发生时执行自定义的代码。

简单来说,监听器就像一个旁听者,默默地观察着测试的运行,并在关键时刻采取行动。

2. PHPUnitFrameworkTestListener 接口

PHPUnitFrameworkTestListener 接口定义了以下方法:

方法名 触发时机 说明
addError(Test $test, Throwable $t, float $time) 测试中发生错误(Error)时 当测试代码中抛出未捕获的异常或发生致命错误时触发。 $test 是发生错误的测试对象, $t 是 Throwable 对象, $time 是测试花费的时间。
addWarning(Test $test, Warning $e, float $time) 测试中发生警告(Warning)时 当测试代码中触发了一个警告时触发。 $test 是发生警告的测试对象, $e 是 Warning 对象, $time 是测试花费的时间。
addFailure(Test $test, AssertionFailedError $e, float $time) 测试断言失败(Failure)时 当测试的断言失败时触发。 $test 是断言失败的测试对象, $e 是 AssertionFailedError 对象, $time 是测试花费的时间。
addIncompleteTest(Test $test, Throwable $t, float $time) 测试被标记为不完整(Incomplete)时 当测试被 @incomplete 注解标记时触发。 $test 是不完整的测试对象, $t 是 Throwable 对象, $time 是测试花费的时间。
addSkippedTest(Test $test, Throwable $t, float $time) 测试被跳过(Skipped)时 当测试被 @skip@requires 注解标记时触发。 $test 是被跳过的测试对象, $t 是 Throwable 对象, $time 是测试花费的时间。
startTestSuite(TestSuite $suite) 测试套件开始运行时 在测试套件开始运行之前触发。 $suite 是即将运行的测试套件对象。
endTestSuite(TestSuite $suite) 测试套件运行结束后 在测试套件运行结束后触发。 $suite 是已经运行的测试套件对象。
startTest(Test $test) 测试开始运行时 在每个测试方法开始运行之前触发。 $test 是即将运行的测试对象。
endTest(Test $test, float $time) 测试运行结束后 在每个测试方法运行结束后触发。 $test 是已经运行的测试对象, $time 是测试花费的时间。

3. 创建自定义监听器

下面是一个简单的自定义监听器的例子,它可以记录每个测试的开始和结束时间:

<?php

use PHPUnitFrameworkTest;
use PHPUnitFrameworkTestSuite;
use PHPUnitFrameworkTestListener;

class TimeTrackerListener implements TestListener
{
    private $startTime;

    public function startTestSuite(TestSuite $suite): void
    {
        echo "Starting Test Suite: " . $suite->getName() . "n";
    }

    public function endTestSuite(TestSuite $suite): void
    {
        echo "Ending Test Suite: " . $suite->getName() . "n";
    }

    public function startTest(Test $test): void
    {
        $this->startTime = microtime(true);
        echo "Starting Test: " . $test->getName() . "n";
    }

    public function endTest(Test $test, float $time): void
    {
        $endTime = microtime(true);
        $duration = $endTime - $this->startTime;
        echo "Ending Test: " . $test->getName() . " (Duration: " . number_format($duration, 4) . " seconds)n";
    }

    public function addError(Test $test, Throwable $t, float $time): void {}
    public function addWarning(Test $test, Warning $e, float $time): void {}
    public function addFailure(Test $test, AssertionFailedError $e, float $time): void {}
    public function addIncompleteTest(Test $test, Throwable $t, float $time): void {}
    public function addSkippedTest(Test $test, Throwable $t, float $time): void {}
}

这个监听器实现了 TestListener 接口的所有方法,但在 startTestendTest 方法中记录了测试的开始和结束时间,并在控制台输出。 其他方法为空,因为我们不需要处理其他事件。

4. 配置和使用监听器

有两种方法可以配置PHPUnit使用监听器:

  • 命令行参数: 使用 --listener 参数指定监听器类名。
  • phpunit.xml配置文件:phpunit.xml 文件中配置监听器。

命令行参数:

./vendor/bin/phpunit --listener TimeTrackerListener tests

phpunit.xml配置文件:

<phpunit>
    <listeners>
        <listener class="TimeTrackerListener"/>
    </listeners>
</phpunit>

将上述 XML 添加到你的 phpunit.xml 文件中,然后运行PHPUnit:

./vendor/bin/phpunit tests

运行结果会显示每个测试的开始和结束时间。

5. 监听器的高级用法

  • 自定义报告: 可以利用监听器生成自定义格式的测试报告,例如 JSON、XML 或 HTML。
  • 集成外部工具: 可以利用监听器将测试结果发送到外部工具,例如持续集成服务器、代码覆盖率工具或缺陷跟踪系统。
  • 执行自定义逻辑: 可以在测试生命周期的各个阶段执行自定义逻辑,例如初始化数据库连接、清理临时文件或发送通知。

6. 扩展(Extension)的概念

PHPUnit的扩展是一种特殊的监听器,它实现了 PHPUnitExtensionExtension 接口。与普通的监听器不同,扩展可以访问PHPUnit的内部状态,并可以注册命令行选项。

扩展提供了一种更灵活的方式来定制PHPUnit的行为。

7. PHPUnitExtensionExtension 接口

PHPUnitExtensionExtension 接口定义了以下方法:

方法名 说明
__construct(string $resultFile, bool $verbose, bool $debug) 构造函数。 $resultFile 是结果文件的路径, $verbose 指示是否启用详细输出, $debug 指示是否启用调试模式。
write(TestResult $result) 将测试结果写入文件。 $result 是测试结果对象。
endTest(Test $test, float $time) 测试运行结束后触发。 $test 是已经运行的测试对象, $time 是测试花费的时间。

注意: PHPUnitExtensionExtension 接口在PHPUnit 9.0之后已被弃用。现在推荐使用PHPUnitFrameworkTestListener接口配合PHPUnitRunnerExtensionFacade类来实现类似的功能。这里主要介绍老版本是为了理解扩展的设计思想。

8. 创建自定义扩展 (使用已弃用的 PHPUnitExtensionExtension)

下面是一个使用已弃用的 PHPUnitExtensionExtension 接口的自定义扩展的例子,它可以将测试结果写入XML文件:

<?php

use PHPUnitFrameworkTestResult;
use PHPUnitFrameworkTest;
use PHPUnitExtensionExtension;

class XmlResultPrinter extends Extension
{
    private $resultFile;

    public function __construct(string $resultFile, bool $verbose, bool $debug)
    {
        $this->resultFile = $resultFile;
    }

    public function write(TestResult $result): void
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        $root = $dom->createElement('testsuites');
        $dom->appendChild($root);

        foreach ($result->suite()->tests() as $test) {
            $testsuite = $dom->createElement('testsuite');
            $testsuite->setAttribute('name', $test->getName());
            $root->appendChild($testsuite);

            $testcase = $dom->createElement('testcase');
            $testcase->setAttribute('name', $test->getName());
            $testcase->setAttribute('class', get_class($test));
            $testsuite->appendChild($testcase);

            if ($result->failureCount() > 0) {
                foreach ($result->failures() as $failure) {
                    if ($failure->testName() == $test->getName()) {
                        $failureElement = $dom->createElement('failure');
                        $failureElement->setAttribute('message', $failure->exceptionAsString());
                        $testcase->appendChild($failureElement);
                    }
                }
            }
        }

        $dom->formatOutput = true;
        $dom->save($this->resultFile);
    }

    public function endTest(Test $test, float $time): void {} // Required, but not used
}

9. 配置和使用扩展 (使用已弃用的 PHPUnitExtensionExtension)

使用扩展需要在命令行中指定扩展类名和结果文件:

./vendor/bin/phpunit --extension XmlResultPrinter --log-junit result.xml tests

注意: 这种方式在PHPUnit 9.0之后会产生警告,因为 PHPUnitExtensionExtension 接口已被弃用。

10. 使用 PHPUnitFrameworkTestListenerPHPUnitRunnerExtensionFacade 实现类似扩展的功能

为了适应PHPUnit 9.0及以后的版本,我们需要使用 PHPUnitFrameworkTestListener 接口和 PHPUnitRunnerExtensionFacade 类来实现类似扩展的功能。

首先,创建一个监听器类:

<?php

use PHPUnitFrameworkTest;
use PHPUnitFrameworkTestSuite;
use PHPUnitFrameworkTestListener;
use PHPUnitFrameworkTestResult;
use PHPUnitFrameworkAssertionFailedError;
use PHPUnitRunnerExtensionFacade;

class ModernXmlResultPrinter implements TestListener
{
    private $resultFile;
    private $testResults = [];

    public function __construct(string $resultFile)
    {
        $this->resultFile = $resultFile;
    }

    public function startTestSuite(TestSuite $suite): void {}
    public function endTestSuite(TestSuite $suite): void {}

    public function startTest(Test $test): void
    {
        $this->testResults[$test->getName()] = ['status' => 'passed', 'message' => ''];
    }

    public function endTest(Test $test, float $time): void
    {
        // No action needed here.  Status is already passed unless an error/failure occurred.
    }

    public function addError(Test $test, Throwable $t, float $time): void
    {
        $this->testResults[$test->getName()] = ['status' => 'error', 'message' => $t->getMessage()];
    }

    public function addWarning(Test $test, Warning $e, float $time): void
    {
        $this->testResults[$test->getName()] = ['status' => 'warning', 'message' => $e->getMessage()];
    }

    public function addFailure(Test $test, AssertionFailedError $e, float $time): void
    {
        $this->testResults[$test->getName()] = ['status' => 'failure', 'message' => $e->getMessage()];
    }

    public function addIncompleteTest(Test $test, Throwable $t, float $time): void
    {
        $this->testResults[$test->getName()] = ['status' => 'incomplete', 'message' => $t->getMessage()];
    }

    public function addSkippedTest(Test $test, Throwable $t, float $time): void
    {
        $this->testResults[$test->getName()] = ['status' => 'skipped', 'message' => $t->getMessage()];
    }

    public function __destruct()
    {
        $this->writeResults();
    }

    private function writeResults(): void
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        $root = $dom->createElement('testsuites');
        $dom->appendChild($root);

        foreach ($this->testResults as $testName => $result) {
            $testsuite = $dom->createElement('testsuite');
            $testsuite->setAttribute('name', $testName);
            $root->appendChild($testsuite);

            $testcase = $dom->createElement('testcase');
            $testcase->setAttribute('name', $testName);
            $testcase->setAttribute('class', 'UnknownClass'); // Replace with actual class if available
            $testsuite->appendChild($testcase);

            if ($result['status'] !== 'passed') {
                $failureElement = $dom->createElement($result['status']); // Use status as element name
                $failureElement->setAttribute('message', $result['message']);
                $testcase->appendChild($failureElement);
            }
        }

        $dom->formatOutput = true;
        $dom->save($this->resultFile);
    }
}

然后,在 phpunit.xml 文件中注册监听器,并使用 <arguments> 标签将结果文件路径传递给构造函数:

<phpunit>
    <listeners>
        <listener class="ModernXmlResultPrinter">
            <arguments>
                <string>result.xml</string>
            </arguments>
        </listener>
    </listeners>
</phpunit>

现在,运行PHPUnit:

./vendor/bin/phpunit tests

这个方法在PHPUnit 9.0及以后的版本中有效,并且避免了使用已弃用的 PHPUnitExtensionExtension 接口。

11. 核心概念总结

总的来说,PHPUnit的监听器和扩展机制为我们提供了强大的定制能力。通过监听器,我们可以监控测试执行的各个阶段,并执行自定义逻辑,例如生成自定义报告、集成外部工具或发送通知。对于较新版本的PHPUnit,推荐使用PHPUnitFrameworkTestListener接口和PHPUnitRunnerExtensionFacade类来实现类似扩展的功能,以避免使用已弃用的 PHPUnitExtensionExtension 接口。

12. 选择合适的机制

  • 监听器: 适用于需要在测试生命周期的各个阶段执行自定义逻辑,但不需要访问PHPUnit内部状态的场景。
  • 扩展 (使用 PHPUnitFrameworkTestListenerPHPUnitRunnerExtensionFacade): 适用于需要在测试生命周期的各个阶段执行自定义逻辑,并且需要访问PHPUnit内部状态的场景。

13. 最佳实践

  • 保持监听器/扩展的代码简洁,避免在监听器/扩展中执行复杂的逻辑。
  • 使用配置文件来管理监听器/扩展,避免在命令行中传递大量的参数。
  • 充分利用PHPUnit提供的API,例如 TestResult 对象,来获取测试结果信息。
  • 编写单元测试来确保监听器/扩展的正确性。

通过灵活运用监听器和扩展,我们可以更好地控制PHPUnit的行为,并将其集成到我们的开发工作流程中,从而提高开发效率和代码质量。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注