Расширение PHPUnit
Улучшение конкретных тестовых случаев
Вы можете расширить PHPUnit, добавив в конкретные тестовые случаи методы, которые предоставляют дополнительную функциональность.
Например, можно использовать проверку в конкретном тестовом случае, чтобы убедиться, что значение, созданное тестируемой системой, соответствует регулярному выражению.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorTest extends TestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$orderId,
),
);
}
}
Вы можете улучшить этот конкретный тестовый случай, выделив проверку, специфичную для предметной области.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorWithDomainSpecificAssertionTest extends TestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
private function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
Выделение абстрактных тестовых случаев
Вы можете расширить PHPUnit, выделив абстрактные тестовые случаи, чтобы совместно использовать функциональность с другими конкретными тестовыми случаями посредством вертикального наследования.
Например, можно перенести упомянутую выше проверку, специфичную для предметной области, в абстрактный тестовый случай.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
abstract class AbstractTestCase extends TestCase
{
final protected function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
Затем вы можете улучшить конкретный тестовый случай, унаследовав его от абстрактного тестового случая.
<?php declare(strict_types=1);
final class OrderIdGeneratorExtendingAbstractTestCaseTest extends AbstractTestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
Выделение трейтов
Вы можете расширить PHPUnit, выделив трейты, чтобы совместно использовать функциональность с конкретными тестовыми случаями посредством горизонтального наследования.
Например, можно перенести упомянутую выше проверку, специфичную для предметной области, в трейт.
<?php declare(strict_types=1);
trait AssertionTrait
{
final protected function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
Затем вы можете улучшить конкретный тестовый случай, подключив трейт.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorUsingAssertionTraitTest extends TestCase
{
use AssertionTrait;
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
Реализация пользовательских ограничений
Вы можете расширить PHPUnit, реализовав пользовательское ограничение. Пользовательское ограничение — это класс, наследующий класс PHPUnit\Framework\Constraint\Constraint.
Пользовательское ограничение должно реализовать два метода:
Метод matches(mixed $other): bool содержит логику проверки. Он возвращает true, если ограничение выполнено, и false в противном случае.
Метод toString(): string возвращает строковое представление ограничения. Эта строка используется в сообщениях об ошибках, когда проверка с этим ограничением завершается неудачей.
Например, можно реализовать ограничение, которое проверяет, является ли строка допустимым идентификатором заказа.
<?php declare(strict_types=1);
use PHPUnit\Framework\Constraint\Constraint;
final class IsValidOrderId extends Constraint
{
public function toString(): string
{
return 'is a valid order ID';
}
protected function matches(mixed $other): bool
{
return is_string($other)
&& preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}$/', $other) === 1;
}
}
Пользовательское ограничение можно использовать с assertThat().
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class IsValidOrderIdTest extends TestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertThat($orderId, new IsValidOrderId);
}
}
Для более точного управления сообщениями об ошибках можно дополнительно переопределить эти методы в классе PHPUnit\Framework\Constraint\Constraint:
Метод failureDescription(mixed $other): string возвращает описание ошибки, если ограничение не выполнено. По умолчанию этот метод объединяет строковое представление проверяемого значения со строкой, возвращаемой методом toString().
Метод additionalFailureDescription(mixed $other): string можно использовать для добавления в сообщение об ошибке дополнительной информации, например различий.
Список всех встроенных ограничений приведен в приложении.
Реализация пользовательских проверок
Вы можете объединить пользовательское ограничение с трейтом или абстрактным тестовым случаем (как показано выше), чтобы создать повторно используемую пользовательскую проверку.
Например, можно обернуть упомянутое выше ограничение IsValidOrderId в трейт, предоставляющий метод assertStringIsOrderId().
<?php declare(strict_types=1);
trait CustomAssertionTrait
{
final protected static function assertStringIsOrderId(string $value, string $message = ''): void
{
static::assertThat($value, new IsValidOrderId, $message);
}
}
Затем этот трейт можно использовать в конкретном тестовом случае.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorUsingCustomAssertionTraitTest extends TestCase
{
use CustomAssertionTrait;
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
Этот подход дает лучшее из двух миров: удобный метод проверки для авторов тестов и хорошо структурированное ограничение с понятными сообщениями об ошибках.
Реализация пользовательских компараторов
Пользовательский компаратор определяет, как assertEquals() и assertNotEquals() сравнивают объекты определенного типа.
Пользовательский компаратор — это класс, наследующий класс SebastianBergmann\Comparator\Comparator и реализующий два метода:
Метод accepts(mixed $expected, mixed $actual): bool возвращает true, если этот компаратор может обработать заданную пару значений.
Метод assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void выполняет сравнение. Если значения не равны, он должен выбросить исключение SebastianBergmann\Comparator\ComparisonFailure.
Рассмотрим, например, объект-значение Money.
<?php declare(strict_types=1);
final readonly class Money
{
private int $amount;
private string $currency;
public function __construct(int $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
public function amount(): int
{
return $this->amount;
}
public function currency(): string
{
return $this->currency;
}
}
Можно реализовать компаратор, сравнивающий объекты Money по сумме и валюте.
<?php declare(strict_types=1);
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;
final class MoneyComparator extends Comparator
{
public function accepts(mixed $expected, mixed $actual): bool
{
return $expected instanceof Money && $actual instanceof Money;
}
public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void
{
if ($expected->amount() !== $actual->amount() ||
$expected->currency() !== $actual->currency()) {
throw new ComparisonFailure(
$expected,
$actual,
$this->exporter()->export($expected),
$this->exporter()->export($actual),
'Failed asserting that two Money objects are equal.',
);
}
}
}
Прежде чем использовать пользовательский компаратор, его необходимо зарегистрировать в SebastianBergmann\Comparator\Factory. После использования его следует снять с регистрации. Лучше всего сделать это в методе, выполняемом перед тестом, например setUp(), и в методе, выполняемом после теста, например tearDown(), в вашем тестовом случае.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use SebastianBergmann\Comparator\Factory;
final class MoneyComparatorTest extends TestCase
{
private MoneyComparator $comparator;
protected function setUp(): void
{
$this->comparator = new MoneyComparator;
Factory::getInstance()->register($this->comparator);
}
protected function tearDown(): void
{
Factory::getInstance()->unregister($this->comparator);
}
public function testMoneyObjectsWithSameAmountAndCurrencyAreEqual(): void
{
$this->assertEquals(
new Money(100, 'EUR'),
new Money(100, 'EUR'),
);
}
}
После регистрации пользовательский компаратор будет использоваться при каждом вызове assertEquals() или assertNotEquals() с двумя объектами Money.
Альтернатива для простых случаев
Если у вашего объекта-значения есть метод equals() (или аналогичный метод), рассмотрите возможность использования assertObjectEquals() вместо реализации пользовательского компаратора. Подробнее см. в описании assertObjectEquals().
Настройка вызова метода теста
Вы можете расширить PHPUnit, переопределив метод TestCase::invokeTestMethod() в классе тестового случая.
Метод invokeTestMethod() отвечает за вызов метода теста. По умолчанию он просто вызывает метод теста с переданными аргументами:
protected function invokeTestMethod(string $methodName, array $testArguments): mixed
{
return $this->{$methodName}(...$testArguments);
}
Фреймворки, которым требуются особые контексты выполнения, например асинхронные фреймворки, могут переопределить этот метод, чтобы обернуть вызов метода теста в планировщик времени выполнения.
Расширение средства запуска тестов
Вы можете расширить PHPUnit, реализовав и зарегистрировав расширение.
Реализация расширения
Расширение PHPUnit — это класс, реализующий интерфейс PHPUnit\Runner\Extension\Extension.
Интерфейс расширения объявляет метод bootstrap(), принимающий конфигурацию PHPUnit, фасад расширения и коллекцию параметров расширения.
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
final class ExampleExtension implements Extension
{
public function bootstrap(
Configuration $configuration,
Facade $facade,
ParameterCollection $parameters
): void {
if ($configuration->noOutput()) {
return;
}
$message = 'the-default-message';
if ($parameters->has('message')) {
$message = $parameters->get('message');
}
$facade->registerSubscriber(new ExampleSubscriber($message));
$facade->registerTracer(new ExampleTracer);
}
}
Конфигурация PHPUnit является экземпляром PHPUnit\TextUI\Configuration\Configuration и предоставляет доступ к конфигурации PHPUnit после объединения параметров конфигурации по умолчанию, XML-файла конфигурации и параметров командной строки.
Вы можете изучить объект конфигурации, чтобы настроить поведение расширения. Например, вы можете захотеть расширить PHPUnit с помощью расширения, выводящего данные в консоль. В этом случае вам может быть полезно узнать, хочет ли пользователь PHPUnit использовать цвета или предпочитает монохромный вывод.
Коллекция параметров является экземпляром PHPUnit\Runner\Extension\ParameterCollection и предоставляет доступ к параметрам расширения, заданным пользователем в XML-файле конфигурации PHPUnit. С помощью коллекции параметров вы можете позволить пользователям расширения настраивать его поведение.
Примечание
Вы должны самостоятельно проверять и обрабатывать значения из коллекции параметров. В PHPUnit нет функциональности для проверки значений из коллекции параметров или приведения их к другим типам.
Фасад расширения является экземпляром PHPUnit\Runner\Extension\Facade и позволяет регистрировать подписчиков событий и трассировщики событий с помощью методов registerSubscribers(), registerSubscriber() и registerTracer().
Фасад расширения также предоставляет следующие методы, с помощью которых расширения средства запуска тестов могут сообщить ему о намерении заменить стандартную функциональность или необходимости включить определенную функциональность:
Метод replacesProgressOutput() можно использовать, чтобы отключить стандартный вывод хода выполнения тестов средством запуска тестов.
Метод replacesResultOutput() можно использовать, чтобы отключить стандартный вывод результатов после завершения запуска тестов.
Метод replacesOutput() объединяет эффекты методов replacesProgressOutput() и replacesResultOutput() (см. выше).
Метод requiresCodeCoverageCollection() можно использовать, чтобы включить сбор данных о покрытии кода.
Метод requiresExportOfObjects() можно использовать, чтобы включить экспорт объектов, например, для таких событий, как Test\AssertionSucceeded и Test\AssertionFailed.
Реализация подписчика событий
Подписчик событий — это класс, реализующий интерфейс подписчика событий.
Интерфейс подписчика событий объявляет единственный метод notify(), принимающий экземпляр соответствующего класса события.
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;
final class ExampleSubscriber implements ExecutionFinishedSubscriber
{
public function __construct(private readonly string $message)
{
}
public function notify(ExecutionFinished $event): void
{
print __METHOD__ . PHP_EOL . $this->message . PHP_EOL;
}
}
После регистрации подписчика событий через фасад расширения PHPUnit будет уведомлять его при отправке события соответствующего класса.
Примечание
Нельзя создать подписчик событий, реализующий одновременно несколько интерфейсов подписчика событий.
Если вы хотите подписаться более чем на одно событие, необходимо реализовать как минимум одного подписчика для каждого интересующего вас события.
Реализация трассировщика событий
Трассировщик событий — это класс, реализующий интерфейс PHPUnit\Event\Tracer\Tracer.
Интерфейс трассировщика объявляет единственный метод trace(), принимающий событие.
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Event\Event;
use PHPUnit\Event\Tracer\Tracer;
final class ExampleTracer implements Tracer
{
public function trace(Event $event): void
{
// ...
}
}
После регистрации трассировщика событий через фасад расширения PHPUnit будет уведомлять его о каждом событии.
Подсказка
Не уверены, следует ли реализовать трассировщик событий или несколько подписчиков событий?
Если вас интересует каждое событие, отправляемое PHPUnit во время выполнения приложения CLI, вероятно, вам следует реализовать и зарегистрировать трассировщик событий.
Если вас интересуют отдельные события, отправляемые PHPUnit во время выполнения приложения CLI, вероятно, вам следует реализовать и зарегистрировать одного или нескольких подписчиков событий.
Понимание событий
Событие — это класс, реализующий интерфейс PHPUnit\Event\Event.
Интерфейс PHPUnit\Event\Event объявляет метод telemetryInfo(), предоставляющий доступ к данным телеметрии, и метод asString(), возвращающий строковое представление события.
Каждое событие может реализовывать дополнительные методы, предоставляющие доступ к информации, доступной при регистрации и отправке события PHPUnit.
Вы можете получать, изучать и обрабатывать эти события в подписчиках или трассировщиках событий.
Список всех событий, которые в настоящее время отправляет PHPUnit, приведен в приложении.
Примечание
В настоящее время PHPUnit не поддерживает регистрацию пользовательских событий.
Полный пример: пользовательский принтер
Стандартный вывод PHPUnit показывает во время выполнения тестов индикатор выполнения из точек и букв, а затем подробную сводку результатов. Расширение может полностью подавить этот вывод и заменить его собственным, вызвав replaceOutput() для фасада расширения во время загрузки. В этом разделе на полном примере объясняется каждый необходимый элемент.
Что мы создаем
Пример расширения выводит по одной строке на каждый тест в следующем формате:
ExampleTest::testPasses ... passed (0.001s)
ExampleTest::testFails ... failed (0.002s)
ExampleTest::testIsSkipped ... skipped (0.001s)
ExampleTest::testIsIncomplete ... incomplete (0.001s)
Пример намеренно сделан минималистичным, чтобы сосредоточиться на инфраструктуре расширения, а не на деталях форматирования.
Структура расширения
Расширение состоит из четырех видов объектов:
Расширение — точка входа, которую загружает PHPUnit
Принтер — содержит логику вывода, записывая данные в
STDOUTПодписчики — по одному на каждое событие теста; каждый передает соответствующие данные в
PrinterКонфигурация — файл
phpunit.xml, в котором регистрируется расширение
Шаг 1. Реализуйте точку входа расширения
Каждое расширение PHPUnit должно содержать класс, реализующий PHPUnit\Runner\Extension\Extension:
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer;
use PHPUnit\Runner\Extension\Extension as ExtensionInterface;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
final class Extension implements ExtensionInterface
{
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
$facade->replaceOutput();
$printer = new Printer;
$facade->registerSubscribers(
new Subscriber\TestPreparationStartedSubscriber($printer),
new Subscriber\TestPassedSubscriber($printer),
new Subscriber\TestFailedSubscriber($printer),
new Subscriber\TestErroredSubscriber($printer),
new Subscriber\TestSkippedSubscriber($printer),
new Subscriber\TestMarkedIncompleteSubscriber($printer),
new Subscriber\TestFinishedSubscriber($printer),
);
}
}
Интерфейс требует единственного метода bootstrap(), который PHPUnit вызывает один раз после загрузки всех расширений, но до запуска тестов. Метод принимает три аргумента:
$configuration-
Полностью разрешенный объект
Configuration, отражающий настройки изphpunit.xmlи командной строки. Расширение может изучить его, чтобы адаптировать свое поведение, но объект доступен только для чтения. $facade-
Объект
Facade, через который расширение взаимодействует с PHPUnit. ВызовreplaceOutput()для этого объекта сообщает PHPUnit, что нужно подавить собственный вывод хода выполнения и результатов, чтобы расширение могло выводить собственные данные.registerSubscribers()подключает объекты-подписчики расширения к системе событий PHPUnit. $parameters-
Неизменяемая карта ключей и значений, заполненная на основе дочерних элементов
<parameter>, которые могут находиться внутри элемента<bootstrap>вphpunit.xml. Пример расширения не использует параметры.
Примечание
Вызов $facade->replaceOutput() подавляет как вывод хода выполнения, так и вывод результатов, которые в противном случае напечатал бы PHPUnit. Если вы хотите заменить только один из этих видов вывода, вместо этого вызовите $facade->replaceProgressOutput() или $facade->replaceResultOutput().
Шаг 2. Реализуйте принтер
Класс Printer отвечает за весь вывод нашего расширения. Это обычный класс PHP без требований к интерфейсам PHPUnit; подписчики напрямую вызывают его методы:
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer;
use function fwrite;
use function sprintf;
use PHPUnit\Event\Telemetry\HRTime;
final class Printer
{
private ?HRTime $startTime;
private string $outcome;
public function testPreparationStarted(string $testId, HRTime $startTime): void
{
$this->startTime = $startTime;
$this->outcome = 'unknown';
fwrite(
STDOUT,
sprintf(
'%s ... ',
$testId,
),
);
}
public function testOutcome(string $outcome): void
{
$this->outcome = $outcome;
}
public function testFinished(HRTime $endTime): void
{
fwrite(
STDOUT,
sprintf(
'%s (%.3fs)' . PHP_EOL,
$this->outcome,
$endTime->duration($this->startTime)->asFloat(),
),
);
$this->startTime = null;
$this->outcome = 'unknown';
}
}
Класс Printer хранит два значения между началом и завершением каждого теста:
$startTime— экземплярHRTime, сохраняемый при начале теста и используемый для вычисления времени выполнения после завершения теста.$outcome— строка, задаваемая тем подписчиком результата, который сработает (passed,failed,errored,skippedилиincomplete).
При вызове testPreparationStarted() принтер выводит идентификатор теста, а затем `` … `` и оставляет курсор в той же строке. При вызове testFinished() он добавляет результат и время выполнения, а затем переходит на следующую строку. Между этими двумя вызовами для записи результата будет вызван ровно один метод обработки результата.
HRTime::duration() возвращает объект Duration; Duration::asFloat() преобразует его в число секунд типа float, которое sprintf форматирует с точностью до трех знаков после запятой.
Шаг 3. Реализуйте подписчиков
Подписчик — это класс, реализующий один из интерфейсов подписчиков событий PHPUnit. Каждый интерфейс соответствует одному событию и содержит единственный метод notify(), который PHPUnit вызывает при возникновении события. Подписчик получает объект события и может считывать из него данные.
Все подписчики этого расширения устроены одинаково: каждый хранит ссылку на общий экземпляр Printer и делегирует работу одному из методов принтера.
TestPreparationStartedSubscriber
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\PreparationStartedSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestPreparationStartedSubscriber implements PreparationStartedSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(PreparationStarted $event): void
{
$this->printer->testPreparationStarted(
$event->test()->id(),
$event->telemetryInfo()->time()
);
}
}
Этот подписчик срабатывает непосредственно перед подготовкой теста в PHPUnit (до выполнения setUp()). Он считывает идентификатор теста из $event->test()->id() — метод возвращает строку, например ExampleTest::testPasses, — и текущее время с высокой точностью из $event->telemetryInfo()->time().
Подписчики результатов
После завершения теста могут возникнуть четыре события результата — по одному для каждого возможного результата. Каждое событие обрабатывает отдельный подписчик, который вызывает Printer::testOutcome() с соответствующей меткой.
TestPassedSubscriber — срабатывает, если все проверки пройдены:
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\Passed;
use PHPUnit\Event\Test\PassedSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestPassedSubscriber implements PassedSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(Passed $event): void
{
$this->printer->testOutcome('passed');
}
}
TestFailedSubscriber — срабатывает, если проверка завершилась неудачей:
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\FailedSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestFailedSubscriber implements FailedSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(Failed $event): void
{
$this->printer->testOutcome('failed');
}
}
TestErroredSubscriber — срабатывает, если тест выбрасывает неожиданное исключение или вызывает ошибку PHP, не являющуюся неудачей проверки:
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\ErroredSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestErroredSubscriber implements ErroredSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(Errored $event): void
{
$this->printer->testOutcome('errored');
}
}
TestSkippedSubscriber — срабатывает, если тест вызывает $this->markTestSkipped():
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestSkippedSubscriber implements SkippedSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(Skipped $event): void
{
$this->printer->testOutcome('skipped');
}
}
TestMarkedIncompleteSubscriber — срабатывает, если тест вызывает $this->markTestIncomplete():
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestMarkedIncompleteSubscriber implements MarkedIncompleteSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(MarkedIncomplete $event): void
{
$this->printer->testOutcome('incomplete');
}
}
TestFinishedSubscriber
<?php declare(strict_types=1);
namespace PHPUnit\ExtensionExample\Printer\Subscriber;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\ExtensionExample\Printer\Printer;
final readonly class TestFinishedSubscriber implements FinishedSubscriber
{
public function __construct(private Printer $printer) {}
public function notify(Finished $event): void
{
$this->printer->testFinished(
$event->telemetryInfo()->time(),
);
}
}
Этот подписчик срабатывает после завершения tearDown(). Он передает текущее время с высокой точностью в Printer::testFinished(), который использует его вместе с ранее сохраненным временем начала, чтобы вычислить длительность теста.
Примечание
Последовательность событий для одного теста всегда такова:
Test\PreparationStartedРовно одно событие результата:
Test\Passed,Test\Failed,Test\Errored,Test\SkippedилиTest\MarkedIncompleteTest\Finished
Поскольку события результата возникают до Test\Finished, Printer всегда знает результат к моменту вывода завершенной строки.
Шаг 4. Зарегистрируйте расширение в phpunit.xml
PHPUnit обнаруживает расширения через элемент <extensions> в phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunitxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../phpunit.xsd"
bootstrap="autoload.php">
<extensions>
<bootstrapclass="PHPUnit\ExtensionExample\Printer\Extension"/>
</extensions>
<testsuites>
<testsuitename="default">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Атрибут class элемента <bootstrap> должен содержать полное имя класса, реализующего PHPUnit\Runner\Extension\Extension. PHPUnit создает экземпляр этого класса и вызывает его метод bootstrap().
Необязательные параметры можно передать расширению, добавив элементы <parameter> внутрь <bootstrap>:
<bootstrapclass="PHPUnit\ExtensionExample\Printer\Extension">
<parametername="colorize"value="true"/>
</bootstrap>
Затем расширение может прочитать значение параметра в bootstrap() с помощью $parameters->get('colorize'). Сначала всегда вызывайте $parameters->has(), чтобы проверить, был ли задан параметр.
Как все это работает вместе
При запуске PHPUnit с активным примером расширения для каждого теста происходит следующая последовательность событий:
PHPUnit отправляет
Test\PreparationStarted.TestPreparationStartedSubscriberвызываетPrinter::testPreparationStarted(), который сохраняет время начала и выводитExampleTest::testPasses ... `` to ``STDOUTбез завершающего символа новой строки.Выполняется метод теста. В зависимости от его действий PHPUnit отправляет одно событие результата. Соответствующий подписчик вызывает
Printer::testOutcome(), чтобы записать метку результата.
3. PHPUnit отправляет Test\Finished. TestFinishedSubscriber вызывает Printer::testFinished(), который добавляет метку и время выполнения к строке, начатой на шаге 1, а затем сбрасывает состояние принтера.
Поскольку во время загрузки был вызван $facade->replaceOutput(), PHPUnit ничего не выводит. Расширение полностью контролирует содержимое терминала.
Регистрация расширения
Вы можете зарегистрировать одно или несколько расширений PHPUnit из PHAR-архивов или пакетов Composer с помощью элементов extensions, bootstrap и parameters XML-файла конфигурации PHPUnit.
Регистрация расширения из PHAR-архива
Если вы устанавливаете PHPUnit в виде PHAR-архива, лучше загружать расширения из PHAR-архивов.
С помощью атрибута extensionsDirectory элемента phpunit можно указать каталог, из которого PHPUnit должен загружать расширения в виде PHAR-архивов.
<?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"
extensionsDirectory="../phpunit-extensions/"
>
<!-- ... -->
<extensions>
<bootstrapclass="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parametername="message"value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
Регистрация расширения из пакета Composer
Если вы устанавливаете PHPUnit в виде пакета Composer, лучше загружать расширения из пакетов Composer.
Настраивать атрибут extensionsDirectory не требуется, поскольку расширения из пакетов Composer доступны через механизм автозагрузки Composer.
<?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"
>
<!-- ... -->
<extensions>
<bootstrapclass="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parametername="message"value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
Отладка PHPUnit
Параметр CLI --log-events-text средства запуска тестов можно использовать для записи текстового представления каждого события в поток. В приведенном ниже примере мы используем --no-output, чтобы отключить как стандартный вывод хода выполнения, так и стандартный вывод результатов. Затем мы используем --log-events-text php://stdout, чтобы записать сведения о событиях в стандартный вывод:
phpunit --no-output --log-events-text php://stdout
PHPUnit Started (PHPUnit 10.0.0 using PHP 8.2.1 (cli) on Linux)
Test Runner Configured
Test Suite Loaded (2 tests)
Event Facade Sealed
Test Runner Started
Test Suite Sorted
Test Runner Execution Started (2 tests)
Test Suite Started (ExampleTest, 2 tests)
Test Preparation Started (ExampleTest::testOne)
Test Prepared (ExampleTest::testOne)
Assertion Succeeded (Constraint: is true)
Test Passed (ExampleTest::testOne)
Test Finished (ExampleTest::testOne)
Test Preparation Started (ExampleTest::testTwo)
Test Prepared (ExampleTest::testTwo)
Assertion Failed (Constraint: is identical to 'foo', Value: 'bar')
Test Failed (ExampleTest::testTwo)
Failed asserting that two strings are identical.
Test Finished (ExampleTest::testTwo)
Test Suite Finished (ExampleTest, 2 tests)
Test Runner Execution Finished
Test Runner Finished
PHPUnit Finished (Shell Exit Code: 1)
В качестве альтернативы можно использовать параметр CLI --log-events-verbose-text, чтобы включить сведения об использовании ресурсов (время с момента запуска средства запуска тестов, время с момента предыдущего события и использование памяти):
phpunit --no-output --log-events-verbose-text php://stdout
[00:00:00.000046482 / 00:00:00.000006987] [4194304 bytes] PHPUnit Started (PHPUnit 10.0.0 using PHP 8.2.1 (cli) on Linux)
[00:00:00.048195557 / 00:00:00.048149075] [4194304 bytes] Test Runner Configured
[00:00:00.067646038 / 00:00:00.019450481] [6291456 bytes] Test Suite Loaded (2 tests)
[00:00:00.075942220 / 00:00:00.008296182] [6291456 bytes] Event Facade Sealed
[00:00:00.076452360 / 00:00:00.000510140] [6291456 bytes] Test Runner Started
[00:00:00.084421682 / 00:00:00.007969322] [6291456 bytes] Test Suite Sorted
[00:00:00.084664485 / 00:00:00.000242803] [6291456 bytes] Test Runner Execution Started (2 tests)
[00:00:00.085240320 / 00:00:00.000575835] [6291456 bytes] Test Suite Started (ExampleTest, 2 tests)
[00:00:00.086992385 / 00:00:00.001752065] [6291456 bytes] Test Preparation Started (ExampleTest::testOne)
[00:00:00.087443560 / 00:00:00.000451175] [6291456 bytes] Test Prepared (ExampleTest::testOne)
[00:00:00.088237489 / 00:00:00.000793929] [6291456 bytes] Assertion Succeeded (Constraint: is true)
[00:00:00.089076305 / 00:00:00.000838816] [6291456 bytes] Test Passed (ExampleTest::testOne)
[00:00:00.091027624 / 00:00:00.001951319] [6291456 bytes] Test Finished (ExampleTest::testOne)
[00:00:00.091110095 / 00:00:00.000082471] [6291456 bytes] Test Preparation Started (ExampleTest::testTwo)
[00:00:00.091158739 / 00:00:00.000048644] [6291456 bytes] Test Prepared (ExampleTest::testTwo)
[00:00:00.091991799 / 00:00:00.000833060] [6291456 bytes] Assertion Failed (Constraint: is identical to 'foo', Value: 'bar')
[00:00:00.099242925 / 00:00:00.007251126] [8388608 bytes] Test Failed (ExampleTest::testTwo)
Failed asserting that two strings are identical.
[00:00:00.099386498 / 00:00:00.000143573] [8388608 bytes] Test Finished (ExampleTest::testTwo)
[00:00:00.099437634 / 00:00:00.000051136] [8388608 bytes] Test Suite Finished (ExampleTest, 2 tests)
[00:00:00.103014760 / 00:00:00.003577126] [8388608 bytes] Test Runner Execution Finished
[00:00:00.103207309 / 00:00:00.000192549] [8388608 bytes] Test Runner Finished
[00:00:00.105879902 / 00:00:00.002672593] [8388608 bytes] PHPUnit Finished (Shell Exit Code: 1)
Оборачивание средства запуска тестов
Класс PHPUnit\TextUI\Application является точкой входа собственного средства запуска тестов CLI PHPUnit. Он не предназначен для повторного использования разработчиками, которые хотят обернуть PHPUnit, чтобы создать что-то вроде ParaTest.
Для фактического запуска тестов PHPUnit\TextUI\Application использует PHPUnit\TextUI\TestRunner::run().
Для PHPUnit\TextUI\TestRunner::run() необходимы PHPUnit\TextUI\Configuration\Configuration, PHPUnit\Runner\ResultCache\ResultCache и PHPUnit\Framework\TestSuite.
Объект PHPUnit\TextUI\Configuration\Configuration можно создать с помощью PHPUnit\TextUI\Configuration\Builder::build(). Этому методу необходимо передать $_SERVER['argv']. Затем метод разбирает аргументы и параметры CLI и загружает XML-файл конфигурации, если его удается загрузить.
Объект PHPUnit\Framework\TestSuite можно создать из PHPUnit\TextUI\Configuration\Configuration с помощью PHPUnit\TextUI\Configuration\TestSuiteBuilder::build().
Несмотря на пометку @internal, PHPUnit\TextUI\TestRunner предназначен для повторного использования разработчиками, которые хотят обернуть средство запуска тестов PHPUnit.
© 2005–2026 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://docs.phpunit.de/en/13.2/extending-phpunit.html