12. Расширение PHPUnit

PHPUnit можно расширить различными способами для облегчения процесса написания тестов и настройки обратной связи, получаемой от выполнения тестов. Вот общие отправные точки для расширения PHPUnit.

Подкласс PHPUnit\Framework\TestCase

Написать пользовательские утверждения и вспомогательные методы в абстрактных подклассах PHPUnit\Framework\TestCase и наследуйте ваши классы тестов от этого класса. Это один из самых простых способов расширения PHPUnit.

Написание пользовательских утверждений

При написании пользовательских утверждений лучше всего следовать принципам реализации собственных утверждений PHPUnit. Как вы можете видеть в Пример 12.1, метод assertTrue() — это просто обёртка над методами isTrue() и assertThat(): isTrue() создаёт объект сопоставления, который передаётся assertThat() для проведения вычисления.

Пример 12.1 Методы assertTrue() и isTrue() класса PHPUnit\Framework\Assert
<?php
namespace PHPUnit\Framework;

use PHPUnit\Framework\TestCase;

abstract class 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;
    }

    // ...
}

Пример 12.2 показывает, как PHPUnit\Framework\Constraint\IsTrue наследует абстрактный базовый класс для объектов сопоставления (или ограничений), PHPUnit\Framework\Constraint.

Пример 12.2 Класс PHPUnit\Framework\Constraint\IsTrue
<?php
namespace PHPUnit\Framework\Constraint;

use PHPUnit\Framework\Constraint;

class IsTrue extends Constraint
{
    /**
     * Вычисляет ограничение для параметра $other. Возвращает true, если
     * ограничение удовлетворяется, в противном случае — false.
     *
     * @param mixed $other Значение или объект для вычисления
     * @return bool
     */
    public function matches($other)
    {
        return $other === true;
    }

    /**
     * Возвращает ограничения в виде строки
     *
     * @return string
     */
    public function toString()
    {
        return 'это true';
    }
}

Усилия по реализации методов assertTrue() и isTrue(), а также класса PHPUnit\Framework\Constraint\IsTrue дают преимущество, состоящее в том, что assertThat() автоматически выполняет вычисление утверждения и задач отчётности, таких как подсчёт статистики. Кроме того, метод isTrue() может использоваться как сопоставление при настройке подставных объектов.

Реализация PHPUnit\Framework\TestListener

Пример 12.3 показывает простую реализацию интерфейса PHPUnit\Framework\TestListener.

Пример 12.3 Простой обработчик тестов
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestListener;

class SimpleTestListener implements TestListener
{
    public function addError(PHPUnit\Framework\Test $test, \Throwable $e, float $time): void
    {
        printf("Ошибка во время выполнения теста '%s'.\n", $test->getName());
    }

    public function addWarning(PHPUnit\Framework\Test $test, PHPUnit\Framework\Warning $e, float $time): void
    {
        printf("Предупреждение во время выполнения теста '%s'.\n", $test->getName());
    }

    public function addFailure(PHPUnit\Framework\Test $test, PHPUnit\Framework\AssertionFailedError $e, float $time): void
    {
        printf("Тест '%s' провалился.\n", $test->getName());
    }

    public function addIncompleteTest(PHPUnit\Framework\Test $test, Exception $e, float $time): void
    {
        printf("Тест '%s' является неполным.\n", $test->getName());
    }

    public function addRiskyTest(PHPUnit\Framework\Test $test, Exception $e, float $time): void
    {
        printf("Тест '%s' считается рискованным.\n", $test->getName());
    }

    public function addSkippedTest(PHPUnit\Framework\Test $test, Exception $e, float $time): void
    {
        printf("Тест '%s' был пропущен.\n", $test->getName());
    }

    public function startTest(PHPUnit\Framework\Test $test): void
    {
        printf("Тест '%s' запустился.\n", $test->getName());
    }

    public function endTest(PHPUnit\Framework\Test $test, float $time): void
    {
        printf("Тест '%s' завершился.\n", $test->getName());
    }

    public function startTestSuite(PHPUnit\Framework\TestSuite $suite): void
    {
        printf("Набор тестов '%s' запустился.\n", $suite->getName());
    }

    public function endTestSuite(PHPUnit\Framework\TestSuite $suite): void
    {
        printf("Набор тестов '%s' завершился.\n", $suite->getName());
    }
}

Пример 12.4 показывает, как использовать трейт PHPUnit\Framework\TestListenerDefaultImplementation, который позволяет указать только интересующие методы интерфейса для вашего случая, но при этом предоставляет пустые реализации для всех остальных методов.

Пример 12.4 Использование трейта с реализацией по умолчанию для обработчика тестов
<?php
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestListenerDefaultImplementation;

class ShortTestListener implements TestListener
{
    use TestListenerDefaultImplementation;

    public function endTest(PHPUnit\Framework\Test $test, $time): void
    {
        printf("Тест '%s' завершился.\n", $test->getName());
    }
}

В Обработчики тестов вы увидите, как настроить PHPUnit для добавления обработчика тестов к выполнению теста.

Реализация PHPUnit\Framework\Test

Интерфейс PHPUnit\Framework\Test — небольшой и простой для реализации. Вы можете написать реализацию PHPUnit\Framework\Test, которая проще, чем PHPUnit\Framework\TestCase, и которая, например, запускает тесты, управляемые данными.

Пример 12.5 показывает класс теста, управляемого данными, который сравнивает значения из CSV-файла, где значения разделены запятой. Каждая строка такого файла выглядит примерно как foo;bar, где первое значение — это то, что мы ожидаем, а второе значение — фактическое.

Пример 12.5 Тест, управляемый данными
<?php
use PHPUnit\Framework\TestCase;

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) {
                $result->addFailure($this, $e, $stopTime);
            }

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

            finally {
                $stopTime = PHP_Timer::stop();
            }

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

        return $result;
    }
}

$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit\TextUI\TestRunner::run($test);
PHPUnit |version|.0 by Sebastian Bergmann and contributors.

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

Расширение TestRunner

PHPUnit latest поддерживает расширения TestRunner, которые привязываются к различным событиям во время выполнения теста. См. Регистрация расширений TestRunner для получения дополнительной информации о регистрации расширений в конфигурационном XML-файле PHPUnit.

Каждое доступное событие, к которому может подключаться расширение, представлено интерфейсом, которое расширению необходимо реализовать. Интерфейсы доступных событий перечисляет доступные события в PHPUnit latest.

Интерфейсы доступных событий

  • AfterIncompleteTestHook
  • AfterLastTestHook
  • AfterRiskyTestHook
  • AfterSkippedTestHook
  • AfterSuccessfulTestHook
  • AfterTestErrorHook
  • AfterTestFailureHook
  • AfterTestWarningHook
  • BeforeFirstTestHook
  • BeforeTestHook

Пример 12.6 показывает пример расширения, реализующего BeforeFirstTestHook и AfterLastTestHook:

Пример 12.6 Пример расширения TestRunner
<?php

namespace Vendor;

use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;

final class MyExtension implements BeforeFirstTestHook, AfterLastTestHook
{
    public function executeAfterLastTest(): void
    {
        // вызывается после последнего выполненного теста
    }

    public function executeBeforeFirstTest(): void
    {
        // вызывается до выполнения первого теста
    }
}