phpunit icon indicating copy to clipboard operation
phpunit copied to clipboard

Event System for extending PHPUnit

Open sebastianbergmann opened this issue 4 years ago • 3 comments

History and Background

The TestListener Interface

First, there was the PHPUnit\Framework\TestListener interface:

interface TestListener
{
    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 addRiskyTest(Test $test, Throwable $t, float $time): void;

    public function addSkippedTest(Test $test, Throwable $t, float $time): void;

    public function startTestSuite(TestSuite $suite): void;

    public function endTestSuite(TestSuite $suite): void;

    public function startTest(Test $test): void;

    public function endTest(Test $test, float $time): void;
}

The TestListener interface violates the Interface Segregation Principle, the "I" in "SOLID". This means that it requires the implementation of many methods, even if the client is not interested in the events they represent.

But the TestListener interface also has a fundamental design flaw: it passes around the Test and TestSuite objects, allowing clients to manipulate the outcome of a test run, for instance. To make things worse, client implementations of TestListener exist that bypass the public API of Test and TestSuite to perform such manipulation. In short, TestListener implementations were use to do more than "just listen".

The TestListener interface is deprecated since PHPUnit 8, it will be removed in PHPUnit 10.

The Hook Interfaces

Back in 2018 and with PHPUnit 7.1, we attempted to make extending PHPUnit's test runner easier with the introduction of the PHPUnit\Runner\Hook interfaces. Here is an example of one of these interfaces:

interface AfterSuccessfulTestHook
{
    public function executeAfterSuccessfulTest(string $test, float $time): void;
}

As you can see, we learned from the painful experience we made with the TestListener interface. The PHPUnit\Runner\Hook interfaces follow the Interface Segregation Principle and clients no longer get access to the real test objects that are used by PHPUnit to run the tests.

However, we did not "think big enough" and the PHPUnit\Runner\Hook interfaces were too limited. Even we did not manage to migrate PHPUnit's own TestListener implementations to the PHPUnit\Runner\Hook interfaces. They were a step in the right direction, but they fell short of actually being useful.

The PHPUnit\Runner\Hook interfaces will be deprecated in PHPUnit 10, they will be removed in PHPUnit 11.

The New Event System

At the EU-FOSSA 2 Hackathon in October 2019, Sebastian Bergmann, Ewout Pieter den Ouden, Andreas Möller, Arne Blankerts, and Stefan Priebsch got together and designed a new system for extending PHPUnit based on events.


Arne Blankerts, Andreas Möller, and Ewout Pieter den Ouden

Photo: Arne Blankerts, Andreas Möller, and Ewout Pieter den Ouden work on the new event system for PHPUnit


Since then, Arne Blankerts and Andreas Möller worked on implementing this new event-based system in a branch. This issue is created as we are getting close to be able to merge this branch.

This issue is a work in progress, more information will be added soon. This information will then find its way into the documentation.

List of Events

TODO

How to write an extension that consumes events

TODO

Roadmap

sebastianbergmann avatar May 20 '21 06:05 sebastianbergmann

Hi Sebastian,

Factory methods for value objects of TestSuite and TestCase classes make TestCase and TestSuite non-extensible. https://github.com/sebastianbergmann/phpunit/blob/1c56cf3b18906109ebc4d78077eb7e3ad3e99181/src/Event/Value/TestSuite/TestSuite.php#L55-L145 https://github.com/sebastianbergmann/phpunit/blob/1c56cf3b18906109ebc4d78077eb7e3ad3e99181/src/Event/Value/Test/TestMethod.php#L43-L67

Would it be possible to implement mechanism for extending class to provide factory method for value objects?

Naktibalda avatar Jan 03 '22 17:01 Naktibalda

Please explain why you want to extend these classes.

sebastianbergmann avatar Jan 04 '22 08:01 sebastianbergmann

No matter if hook, or event.

I don't think that any of this can help for situations like https://github.com/sebastianbergmann/phpunit/issues/4960

And should not be used as an excuse to close feature-requests.

top-master avatar Apr 11 '22 20:04 top-master

@sebastianbergmann this closing means that the event system is now stable? I saw that you heavily refactored it, creating a distinction between outcomes and other events (good choice!), is that permanent now? Can we start building on top of that?

Jean85 avatar Sep 08 '22 07:09 Jean85

I have closed this issue because the initial work has been completed by @theseer and @localheinz a long time ago and their work has been merged a long time ago. Since then, with the exception of #5040, everything in PHPUnit itself has been migrated from TestListener to the event system.

I am reluctant to call the current situation "stable", though, and cannot guarantee that there will be no major changes until we approach a release.

sebastianbergmann avatar Sep 08 '22 07:09 sebastianbergmann

Shall we still use hooks in the meantime? I didn't find documentation regarding the new event system, if I understand correctly

  • TestListener is deprecated and will be remote in PHPUnit 10
  • Hook will be deprecated in PHPUnit 10 and removed in PHPUnit 11
  • The new event system is not stable and not documented (?)

It's a bit annoying to build something new when we know it is deprecated but it seems that is the way to go

BafS avatar Sep 09 '22 02:09 BafS

The TestListener interface as well as the Hook interfaces have been removed in PHPUnit 10. The new system will be documented before PHPUnit 10 is released.

In the meantime, you can look at, for instance, these examples:

sebastianbergmann avatar Sep 09 '22 04:09 sebastianbergmann

Okay, thank you for those information!

BafS avatar Sep 09 '22 08:09 BafS

@sebastianbergmann you mentioned that the new event system would be documented before 10 was released.

I see some documentation here: https://phpunit.readthedocs.io/en/10.0/extending-phpunit.html#phpunit-s-event-system

However, it seems to be missing the basic example of how to use these new events.

How does one refactor:

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

class MyCustomeExtension implements BeforeFirstTestHook, AfterLastTestHook
{

I'm guessing it has something to do with:

PHPUnit\Event\Test\AfterLastTestMethodCalled;
PHPUnit\Event\Test\BeforeFirstTestMethodCalled;

But the implementation is not clear to me. They don't look like attributes... so?

arderyp avatar Feb 09 '23 20:02 arderyp

The documentation has not been updated for PHPUnit 10 yet. The JUnit logger is a good starting point to see how events are used.

sebastianbergmann avatar Feb 09 '23 20:02 sebastianbergmann

okay, i'll have a look at JUnit logger

EDIT: this write-up is very helpful, for anyone else who sees this: https://localheinz.com/articles/2023/02/14/extending-phpunit-with-its-new-event-system/

arderyp avatar Feb 09 '23 20:02 arderyp

I think it is safe to say that this is a topic for 90% of us because of removed functionality that we relied upon. This will probably help too https://docs.phpunit.de/en/10.3/extending-phpunit.html#extending-the-test-runner

bilogic avatar Oct 05 '23 04:10 bilogic