12. Extending PHPUnit
Enhancing concrete test cases
You can extend PHPUnit by enhancing concrete test cases with methods that add functionality.
For example, you may use an assertion in a concrete test case to assert that a value created by the system under test matches a regular expression.
<?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,
),
);
}
}
You can enhance this concrete test case by extracting a domain-specific assertion.
<?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,
),
);
}
}
Extracting abstract test cases
You can extend PHPUnit by extracting abstract test cases to share functionality with other concrete test cases via vertical inheritance.
For example, you may want to pull the domain-specific assertion from above into an abstract test case.
<?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,
),
);
}
}
You can then enhance a concrete test case by extending the abstract test case.
<?php declare(strict_types=1);
final class OrderIdGeneratorExtendingAbstractTestCaseTest extends AbstractTestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
Extracting traits
You can extend PHPUnit by extracting traits to share functionality with concrete test cases via horizontal inheritance.
For example, you may want to pull the domain-specific assertion from above into a trait.
<?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,
),
);
}
}
You can then enhance a concrete test case by using the trait.
<?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);
}
}
Implementing custom constraints
You can extend PHPUnit by implementing a custom constraint.
A custom constraint is a class that extends the PHPUnit\Framework\Constraint\Constraint class.
A custom constraint must implement two methods:
The matches(mixed $other): bool method contains the evaluation logic.
It returns true when the constraint is met and false otherwise.
The toString(): string method returns a string representation of the constraint.
This string is used in failure messages when an assertion using this constraint fails.
For example, you may want to implement a constraint that checks whether a string is a valid order ID.
<?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;
}
}
You can use a custom constraint with 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);
}
}
You can optionally override these methods on the PHPUnit\Framework\Constraint\Constraint class for more control over failure messages:
The failureDescription(mixed $other): string method returns the description of the failure when the constraint is not met.
By default, this method combines the string representation of the evaluated value with the string returned by toString().
The additionalFailureDescription(mixed $other): string method can be used to provide additional details, such as a diff, in the failure message.
You can find a list of all built-in constraints in the appendix.
Implementing custom assertions
You can combine a custom constraint with a trait or abstract test case (as shown above) to create a reusable custom assertion.
For example, you can wrap the IsValidOrderId constraint from above in a trait that provides an assertStringIsOrderId() method.
<?php declare(strict_types=1);
trait CustomAssertionTrait
{
final protected static function assertStringIsOrderId(string $value, string $message = ''): void
{
static::assertThat($value, new IsValidOrderId, $message);
}
}
You can then use this trait in a concrete test case.
<?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);
}
}
This approach gives you the best of both worlds: a convenient assertion method for test authors and a well-structured constraint that provides clear failure messages.
Implementing custom comparators
A custom comparator controls how assertEquals() and assertNotEquals() compare objects of a specific type.
A custom comparator is a class that extends the SebastianBergmann\Comparator\Comparator class that must implement two methods:
The accepts(mixed $expected, mixed $actual): bool method returns true when this comparator can handle the given pair of values.
The assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void method performs the comparison.
It must throw a SebastianBergmann\Comparator\ComparisonFailure exception when the values are not equal.
For example, consider a Money value object.
<?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;
}
}
You may want to implement a comparator that compares Money objects by their amount and currency.
<?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.',
);
}
}
}
You must register a custom comparator with the SebastianBergmann\Comparator\Factory before it can be used.
After you are done, you should unregister it again.
The best place to do this is in a before-test method such as setUp() and an after-test method such as tearDown() methods of your test case.
<?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'),
);
}
}
Once the custom comparator is registered, it will be used whenever assertEquals() or assertNotEquals() is called with two Money objects.
Alternative for simple cases
If your value object has an equals() method (or a similar method), consider using assertObjectEquals() instead of implementing a custom comparator.
See assertObjectEquals() for details.
Implementing custom object exporters
When an assertion fails, PHPUnit exports the values that were involved so that they can be represented as strings in the failure message. By default, an object is exported with all its properties. This can make failure messages hard to read when the objects involved are large or contain properties that are irrelevant for understanding the failure.
A custom object exporter controls how objects of a specific type are represented in failure messages.
A custom object exporter is a class that implements the SebastianBergmann\Exporter\ObjectExporter interface, which declares two methods:
The handles(object $object): bool method returns true when this object exporter can export the given object.
The export(object $object, Exporter $exporter, int $indentation): string method returns the string representation of the given object.
The $exporter argument can be used to export the values of the object’s properties.
The $indentation argument provides the current level of indentation to support nested output.
For example, you may want to represent the Money objects from the previous section compactly by their amount and currency.
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporter;
final class MoneyExporter implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Money;
}
public function export(object $object, Exporter $exporter, int $indentation): string
{
assert($object instanceof Money);
return sprintf(
'Money (%d %s)',
$object->amount(),
$object->currency(),
);
}
}
You can register a custom object exporter using the registerObjectExporter() method of the PHPUnit\Framework\TestCase class.
PHPUnit automatically unregisters custom object exporters after the test has finished.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class MoneyExporterTest extends TestCase
{
public function testArrayContainsMoney(): void
{
$this->registerObjectExporter(new MoneyExporter);
$this->assertContains(
new Money(100, 'EUR'),
[new Money(100, 'USD')],
);
}
}
The assertion in the test shown above fails with the message shown below:
Failed asserting that an array contains Money (100 EUR).
Without the custom object exporter, the same failure would be described using the default representation:
Failed asserting that an array contains Money Object #519 (
'amount' => 100,
'currency' => 'EUR',
).
Custom object exporters are used wherever PHPUnit exports values: in the failure descriptions of constraints as well as in the diffs shown for comparison failures. They are also used for every occurrence of an object: when the same object appears more than once in an exported value, each of its occurrences is exported by the custom object exporter.
More than one custom object exporter can be registered. The object exporter that was registered last is asked first whether it handles an object, so it takes precedence over the object exporters that were registered before it. An object exporter that was registered earlier is used for the objects that the object exporters registered after it do not handle. The same precedence rule applies to custom comparators. Objects that no registered object exporter handles are exported using the default representation.
Customizing test method invocation
You can extend PHPUnit by overriding the TestCase::invokeTestMethod() method in a test case class.
The invokeTestMethod() method is responsible for invoking the test method. By default, it simply
calls the test method with the provided arguments:
protected function invokeTestMethod(string $methodName, array $testArguments): mixed
{
return $this->{$methodName}(...$testArguments);
}
Frameworks that require special execution contexts, such as asynchronous frameworks, can override this method to wrap the test method invocation with their runtime scheduler.
Extending the Test Runner
You can extend PHPUnit by implementing and registering an extension.
Implementing an extension
A PHPUnit extension is a class that implements the PHPUnit\Runner\Extension\Extension interface.
The extension interface declares a bootstrap() method that accepts the PHPUnit configuration, the extension facade, and the extension parameter collection.
<?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);
}
}
The PHPUnit configuration is an instance of PHPUnit\TextUI\Configuration\Configuration and gives you access to the configuration of PHPUnit after merging configuration options from defaults, the XML configuration file, and command-line options.
You can inspect the configuration object to adjust the behavior of your extension. For example, you may want to extend PHPUnit with an extension that renders output on the console. If that is the case, you may be interested to know whether a user of PHPUnit wants to use colors or prefers a monochrome output.
The parameter collection is an instance of PHPUnit\Runner\Extension\ParameterCollection and gives you access to extension parameters a user has provided via PHPUnit’s XML configuration file. You can use the parameter collection to allow users of the extension to configure the behavior of your extension.
Note
You must verify and process the values from the parameter collection yourself. PHPUnit has no functionality for verifying or casting the values from the parameter collection to other types.
The extension facade is an instance of PHPUnit\Runner\Extension\Facade and allows you to register event subscribers and event tracers using the methods registerSubscribers(), registerSubscriber(), and registerTracer().
The extension facade also provides the following methods for test runner extensions to indicate to the test runner that they intend to replace default functionality or require certain functionality to be activated:
The replaceProgressOutput() method can be used to disable the test runner’s default progress output while it runs the tests.
The replaceResultOutput() method can be used to disable the test runner’s default result output after it finished running the tests.
The replaceOutput() method combines the effects of replaceProgressOutput() and replaceResultOutput() (see above).
The requireCodeCoverageCollection() method can be used to activate the collection of code coverage information.
Implementing an event subscriber
An event subscriber is a class that implements an event subscriber interface.
An event subscriber interface declares a single notify() method that accepts an instance of the corresponding event class.
<?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;
}
}
After registering an event subscriber with the extension facade, PHPUnit will notify the event subscriber when emitting an event of the corresponding event class.
Note
You can not create an event subscriber that implements more than one event subscriber interface at a time.
If you want to subscribe to more than one event, you need to implement at least one event subscriber for each event you are interested in.
Implementing an event tracer
An event tracer is a class that implements the PHPUnit\Event\Tracer\Tracer interface.
The tracer interface declares a single trace() method that accepts an event.
<?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
{
// ...
}
}
After registering an event tracer with the extension facade, PHPUnit will notify the tracer of every event.
Hint
Are you unsure whether you should implement an event tracer or multiple event subscribers?
If you are interested in every event that PHPUnit emits during the execution of the CLI application, you probably want to implement and register an event tracer.
If you are interested in selected events that PHPUnit emits during the execution of the CLI application, you probably want to implement and register one or more event subscribers.
Understanding events
An event is a class that implements the PHPUnit\Event\Event interface.
The PHPUnit\Event\Event interface declares a telemetryInfo() method that gives you access to telemetry information and an asString() method that returns a string representation of the event.
Each event may implement additional methods that provide access to information available when PHPUnit registers and emits the event.
You can consume, inspect, and process these events in event subscribers or tracers.
You can find a list of all events PHPUnit currently emits in the appendix.
Note
PHPUnit currently does not support registering custom events.
Passing information from a test to an extension
Sometimes an extension needs information that only the test method that is currently running has. You can pass such information from a test method to an extension using the provideAdditionalInformation() method of PHPUnit\Framework\TestCase.
The provideAdditionalInformation() method accepts a non-empty string. When it is called, PHPUnit emits a PHPUnit\Event\Test\AdditionalInformationProvided event. This method can be called more than once from the same test method; each call emits a separate event.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExampleTest extends TestCase
{
public function testSomething(): void
{
// ...
$this->provideAdditionalInformation(
json_encode(['key' => 'value'], JSON_THROW_ON_ERROR),
);
// ...
}
}
PHPUnit does not interpret the string that is passed to provideAdditionalInformation(). If you want to pass structured information, you need to serialize it, for instance using JSON, and then unserialize it in your extension.
To receive this information, an extension registers a subscriber for the PHPUnit\Event\Test\AdditionalInformationProvided event. This event’s test() method returns the test method that provided the information, and its additionalInformation() method returns the string that was passed to provideAdditionalInformation().
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Event\Test\AdditionalInformationProvided;
use PHPUnit\Event\Test\AdditionalInformationProvidedSubscriber;
final class AdditionalInformationSubscriber implements AdditionalInformationProvidedSubscriber
{
public function notify(AdditionalInformationProvided $event): void
{
$test = $event->test();
$information = json_decode(
$event->additionalInformation(),
true,
512,
JSON_THROW_ON_ERROR,
);
// ...
}
}
The article From Events to Insights shows a use case: an abstract test case class for testing event-sourced code uses provideAdditionalInformation() to pass structured information about each test’s Given, When, and Then phases to a PHPUnit extension. The extension then uses this information to generate documentation in Event Storming notation.
A complete example: custom printer
PHPUnit’s default output shows a progress bar of dots and letters while tests run, followed by a detailed result summary.
An extension can suppress that output entirely and substitute its own by calling replaceOutput() on the extension facade during bootstrap.
This section walks through a complete example to explain every piece that is needed.
What we are building
The example extension prints one line per test in the following format:
ExampleTest::testPasses ... passed (0.001s)
ExampleTest::testFails ... failed (0.002s)
ExampleTest::testIsSkipped ... skipped (0.001s)
ExampleTest::testIsIncomplete ... incomplete (0.001s)
It is intentionally minimal so that the focus stays on the extension infrastructure rather than on formatting details.
Structure of the extension
The extension is made up of four kinds of objects:
Extension: the entry point that PHPUnit bootstraps
Printer: contains the output logic, writing to
STDOUTSubscribers: one per test event, each forwarding the relevant data to the
PrinterConfiguration: the
phpunit.xmlfile that registers the extension
Step 1: Implement the extension entry point
Every PHPUnit extension must contain a class that implements 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),
);
}
}
The interface requires a single method, bootstrap(), which PHPUnit calls once after it has loaded all extensions but before it runs any tests.
The method receives three arguments:
$configurationThe fully resolved
Configurationobject that reflects the settings fromphpunit.xmland the command line. The extension can inspect it to adapt its behaviour, but it is read-only.$facadeThe
Facadeobject through which the extension interacts with PHPUnit. CallingreplaceOutput()on it tells PHPUnit to suppress its own progress and result output so the extension can produce its own.registerSubscribers()connects the extension’s subscriber objects to PHPUnit’s event system.$parametersAn immutable key/value map populated from the
<parameter>child elements that can appear inside the<bootstrap>element inphpunit.xml. The example extension does not use parameters.
Note
Calling $facade->replaceOutput() suppresses both the progress output and the result output that PHPUnit would otherwise print.
If you only want to replace one of the two, call $facade->replaceProgressOutput() or $facade->replaceResultOutput() instead.
Step 2: Implement the printer
The Printer class is responsible for all output from our extension.
It is a plain PHP class with no PHPUnit interface requirements; subscribers call its methods directly:
<?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';
}
}
The Printer maintains two pieces of state between the start and the end of each test:
$startTime: anHRTimeinstance captured when the test begins, used to compute the elapsed time when the test finishes.$outcome: a string set by whichever outcome subscriber fires (passed,failed,errored,skipped, orincomplete).
When testPreparationStarted() is called, the printer writes the test identifier followed by `` … `` and leaves the cursor on the same line.
When testFinished() is called, it appends the outcome and elapsed time and moves to the next line. Between those two calls, exactly one outcome method will have been called to record the result.
HRTime::duration() returns a Duration object; Duration::asFloat() converts it to a number of seconds as a float, which sprintf formats to three decimal places.
Step 3: Implement the subscribers
A subscriber is a class that implements one of PHPUnit’s event subscriber interfaces.
Each interface corresponds to one event and has a single method, notify(), that PHPUnit calls when the event fires.
The subscriber receives the event object and can read data from it.
Every subscriber in this extension follows the same pattern: it holds a reference to the shared Printer instance and delegates to one of the printer’s methods.
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()
);
}
}
This subscriber fires just before PHPUnit sets up a test (before setUp() runs).
It reads the test identifier from $event->test()->id() (which returns a string such as ExampleTest::testPasses) and the current high-resolution time from $event->telemetryInfo()->time().
Outcome Subscribers
Four outcome events can fire after a test completes, one for each possible result.
Each is handled by a dedicated subscriber that calls Printer::testOutcome() with the corresponding label.
TestPassedSubscriber fires when all assertions pass:
<?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 fires when an assertion fails:
<?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 fires when the test throws an unexpected exception or a PHP error that is not an assertion failure:
<?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 fires when the test calls $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 fires when the test calls $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(),
);
}
}
This subscriber fires after tearDown() has completed.
It passes the current high-resolution time to Printer::testFinished(), which uses it together with the start time saved earlier to compute the test duration.
Note
The sequence of events for a single test is always:
Test\PreparationStartedExactly one outcome event:
Test\Passed,Test\Failed,Test\Errored,Test\Skipped, orTest\MarkedIncompleteTest\Finished
Because outcome events fire before Test\Finished, the Printer always knows the outcome when it writes the completed line.
Step 4: Register the Extension in phpunit.xml
PHPUnit discovers extensions through the <extensions> element in phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../phpunit.xsd"
bootstrap="autoload.php">
<extensions>
<bootstrap class="PHPUnit\ExtensionExample\Printer\Extension"/>
</extensions>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
The class attribute of <bootstrap> must be the fully qualified class name of the class that implements PHPUnit\Runner\Extension\Extension.
PHPUnit instantiates the class and calls its bootstrap() method.
Optional parameters can be passed to an extension by adding <parameter> elements inside <bootstrap>:
<bootstrap class="PHPUnit\ExtensionExample\Printer\Extension">
<parameter name="colorize" value="true"/>
</bootstrap>
The extension can then read the parameter value in bootstrap() using $parameters->get('colorize').
Always call $parameters->has() first to check whether the parameter was supplied.
How it all fits together
When PHPUnit runs with the example extension active, the following sequence of events takes place for each test:
PHPUnit emits
Test\PreparationStarted.TestPreparationStartedSubscribercallsPrinter::testPreparationStarted(), which saves the start time and writesExampleTest::testPasses ... `` to ``STDOUTwithout a trailing newline.The test method executes. Depending on what the method does, PHPUnit emits one outcome event. The corresponding subscriber calls
Printer::testOutcome()to record the result label.
3. PHPUnit emits Test\Finished.
TestFinishedSubscriber calls Printer::testFinished(), which appends the label and the elapsed time to the line already started in step 1, then resets the printer’s state.
Because $facade->replaceOutput() was called during bootstrap, PHPUnit prints nothing of its own.
The extension has full control over what appears on the terminal.
Registering an extension
You can register one or more PHPUnit extensions from PHARs or from Composer package using the extensions, bootstrap, and parameters elements of the PHPUnit XML configuration file.
Registering an extension from a PHAR
When you install PHPUnit as a PHAR, it is best to load extensions from a PHAR.
You can use the extensionsDirectory attribute of the phpunit element to configure the directory from which PHPUnit should load extensions as a 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>
<bootstrap class="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parameter name="message" value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
Registering an extension from a Composer package
When you install PHPUnit as a Composer package, it is best to load extensions from Composer packages.
You do not need to configure the extensionsDirectory attribute, as extensions from Composer packages will be available through the autoloading mechanism of 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>
<bootstrap class="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parameter name="message" value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
Debugging PHPUnit
The test runner’s --log-events-text CLI option can be used to write a plain text representation
for each event to a stream. In the example shown below, we use --no-output to disable both the
default progress output as well as the default result output. Then we use --log-events-text php://stdout
to write event information to standard output:
phpunit --no-output --log-events-text php://stdout
PHPUnit Started (PHPUnit 13.3.2 using PHP 8.5.10 (cli) on Darwin)
Test Runner Configured
Event Facade Sealed
Test Suite Loaded (2 tests)
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)
Test Passed (ExampleTest::testOne)
Test Finished (ExampleTest::testOne)
Test Preparation Started (ExampleTest::testTwo)
Test Prepared (ExampleTest::testTwo)
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)
Alternatively, the --log-events-verbose-text CLI option can be used to include information
about resource consumption (time since the test runner was started, time since the previous event,
and memory usage):
phpunit --no-output --log-events-verbose-text php://stdout
[00:00:00.000009375 / 00:00:00.000009375] [26671776 bytes] PHPUnit Started (PHPUnit 13.3.2 using PHP 8.5.10 (cli) on Darwin)
[00:00:00.000231875 / 00:00:00.000222500] [26671776 bytes] Test Runner Configured
[00:00:00.000344292 / 00:00:00.000112417] [26671776 bytes] Event Facade Sealed
[00:00:00.000605500 / 00:00:00.000261208] [26671776 bytes] Test Suite Loaded (2 tests)
[00:00:00.000628875 / 00:00:00.000023375] [26671776 bytes] Test Runner Started
[00:00:00.000643083 / 00:00:00.000014208] [26671776 bytes] Test Suite Sorted
[00:00:00.000655000 / 00:00:00.000011917] [26671776 bytes] Test Runner Execution Started (2 tests)
[00:00:00.000666750 / 00:00:00.000011750] [26671776 bytes] Test Suite Started (ExampleTest, 2 tests)
[00:00:00.000716083 / 00:00:00.000049333] [26671776 bytes] Test Preparation Started (ExampleTest::testOne)
[00:00:00.000746167 / 00:00:00.000030084] [26267176 bytes] Test Prepared (ExampleTest::testOne)
[00:00:00.000933000 / 00:00:00.000186833] [26288088 bytes] Test Passed (ExampleTest::testOne)
[00:00:00.000974000 / 00:00:00.000041000] [26288088 bytes] Test Finished (ExampleTest::testOne)
[00:00:00.000991625 / 00:00:00.000017625] [26288088 bytes] Test Preparation Started (ExampleTest::testTwo)
[00:00:00.001014792 / 00:00:00.000023167] [26267984 bytes] Test Prepared (ExampleTest::testTwo)
[00:00:00.001171292 / 00:00:00.000156500] [26308432 bytes] Test Failed (ExampleTest::testTwo)
Failed asserting that two strings are identical.
[00:00:00.001201083 / 00:00:00.000029791] [26308432 bytes] Test Finished (ExampleTest::testTwo)
[00:00:00.001213375 / 00:00:00.000012292] [26308432 bytes] Test Suite Finished (ExampleTest, 2 tests)
[00:00:00.001223625 / 00:00:00.000010250] [26308432 bytes] Test Runner Execution Finished
[00:00:00.001231333 / 00:00:00.000007708] [26308432 bytes] Test Runner Finished
[00:00:00.001253292 / 00:00:00.000021959] [26308432 bytes] PHPUnit Finished (Shell Exit Code: 1)
Wrapping the Test Runner
The PHPUnit\TextUI\Application class is the entry point for PHPUnit’s own CLI test runner.
It is not meant to be (re)used by developers who want to wrap PHPUnit to build something such
as ParaTest.
For the actual running of tests, PHPUnit\TextUI\Application uses PHPUnit\TextUI\TestRunner::run().
PHPUnit\TextUI\TestRunner::run() requires a PHPUnit\TextUI\Configuration\Configuration,
a PHPUnit\Runner\ResultCache\ResultCache, and a PHPUnit\Framework\TestSuite.
A PHPUnit\TextUI\Configuration\Configuration can be built using PHPUnit\TextUI\Configuration\Builder::build().
You need to pass $_SERVER['argv'] to this method. The method then parses CLI arguments/options and loads an XML
configuration file, if one can be loaded.
A PHPUnit\Framework\TestSuite can be built from a PHPUnit\TextUI\Configuration\Configuration using
PHPUnit\TextUI\Configuration\TestSuiteBuilder::build().
While it is marked @internal, PHPUnit\TextUI\TestRunner is meant to be (re)used by developers who
want to wrap PHPUnit’s test runner.