Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHPUnit 8 support for the bridge: SymfonyTestsListenerForV7 implements TestListener that is deprecated, we need to use TestHook instead #33331

Closed
nicolas-grekas opened this issue Aug 25, 2019 · 16 comments
Labels
Help wanted Issues and PRs which are looking for volunteers to complete them. PhpUnitBridge Stalled

Comments

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 25, 2019

SymfonyTestsListenerForV7 implements TestListener that is deprecated, we need to use TestHook instead

/cc @greg0ire maybe?

@nicolas-grekas nicolas-grekas added the Help wanted Issues and PRs which are looking for volunteers to complete them. label Aug 25, 2019
@nicolas-grekas nicolas-grekas changed the title PHPUnit 8 support for the bridge: SymfonyTestsListenerForV7 implements TestListener that is deprecated, we need to use TestHook instead PHPUnit 8 support for the bridge: SymfonyTestsListenerForV7 implements TestListener that is deprecated, we need to use TestHook instead Aug 25, 2019
@greg0ire
Copy link
Contributor

I will give it a try :)

@greg0ire
Copy link
Contributor

First hurdle: it's unclear to me what the migration path is for

public function startTestSuite($suite)
{
$suiteName = $suite->getName();
$this->testsWithWarnings = array();
foreach ($suite->tests() as $test) {
if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) {
continue;
}
if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) {
$test->setPreserveGlobalState(false);
}
}
if (-1 === $this->state) {
echo "Testing $suiteName\n";
$this->state = 0;
if (!class_exists('Doctrine\Common\Annotations\AnnotationRegistry', false) && class_exists('Doctrine\Common\Annotations\AnnotationRegistry')) {
if (method_exists('Doctrine\Common\Annotations\AnnotationRegistry', 'registerUniqueLoader')) {
AnnotationRegistry::registerUniqueLoader('class_exists');
} else {
AnnotationRegistry::registerLoader('class_exists');
}
}
if ($this->skippedFile = getenv('SYMFONY_PHPUNIT_SKIPPED_TESTS')) {
$this->state = 1;
if (file_exists($this->skippedFile)) {
$this->state = 2;
if (!$this->wasSkipped = require $this->skippedFile) {
echo "All tests already ran successfully.\n";
$suite->setTests(array());
}
}
}
$testSuites = array($suite);
for ($i = 0; isset($testSuites[$i]); ++$i) {
foreach ($testSuites[$i]->tests() as $test) {
if ($test instanceof TestSuite) {
if (!class_exists($test->getName(), false)) {
$testSuites[] = $test;
continue;
}
$groups = Test::getGroups($test->getName());
if (\in_array('time-sensitive', $groups, true)) {
ClockMock::register($test->getName());
}
if (\in_array('dns-sensitive', $groups, true)) {
DnsMock::register($test->getName());
}
}
}
}
} elseif (2 === $this->state) {
$skipped = array();
foreach ($suite->tests() as $test) {
if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)
|| isset($this->wasSkipped[$suiteName]['*'])
|| isset($this->wasSkipped[$suiteName][$test->getName()])) {
$skipped[] = $test;
}
}
$suite->setTests($skipped);
}
}
, since BeforeFirstTestHook, which seems to be the best candidate for this, does not have access to the current suite.

Its only method is not even called here: https://github.com/sebastianbergmann/phpunit/blob/154de22df1fda1d67758ab1c417603df7b290809/src/Runner/Hook/TestListenerAdapter.php#L130-L132, but directly in the test runner: https://github.com/sebastianbergmann/phpunit/blob/41c75847e0a7e7df2e0eba6b6edf4d1b99da3103/src/TextUI/TestRunner.php#L535-L539

@greg0ire
Copy link
Contributor

I created this PR in order to clarify things: sebastianbergmann/phpunit#3800

@greg0ire
Copy link
Contributor

Ok so it looks like we should also stop relying on test suites entirely, and find another way to get all tests.

@greg0ire
Copy link
Contributor

greg0ire commented Oct 17, 2019

So why do we rely on test suites?

  1. For starters, we use them to get all tests and disable preserveGlobalState on them, which is a saner default than having it enabled by default. (see [Bridge\PhpUnit] Disable broken auto-require mechanism of phpunit #25032):

foreach ($suite->tests() as $test) {
if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) {
continue;
}
if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) {
$test->setPreserveGlobalState(false);
}
}

  1. It's then used to print the test suite name:

  1. Then, it is used to achieve the feature of replaying skipped tests by replacing the tests with an empty array if the file is present and empty (meaning the test suite was run, and there were no skipped tests):

if ($this->skippedFile = getenv('SYMFONY_PHPUNIT_SKIPPED_TESTS')) {
$this->state = 1;
if (file_exists($this->skippedFile)) {
$this->state = 2;
if (!$this->wasSkipped = require $this->skippedFile) {
echo "All tests already ran successfully.\n";
$suite->setTests(array());
}
}
}

Or by reading the file, and replacing the suite's tests with the tests found in the file:

} elseif (2 === $this->state) {
$skipped = array();
foreach ($suite->tests() as $test) {
if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)
|| isset($this->wasSkipped[$suiteName]['*'])
|| isset($this->wasSkipped[$suiteName][$test->getName()])) {
$skipped[] = $test;
}
}
$suite->setTests($skipped);
}

The last piece of code using test suites is that one:

for ($i = 0; isset($testSuites[$i]); ++$i) {
foreach ($testSuites[$i]->tests() as $test) {
if ($test instanceof TestSuite) {
if (!class_exists($test->getName(), false)) {
$testSuites[] = $test;
continue;
}
$groups = Test::getGroups($test->getName());
if (\in_array('time-sensitive', $groups, true)) {
ClockMock::register($test->getName());
}
if (\in_array('dns-sensitive', $groups, true)) {
DnsMock::register($test->getName());
}
}
}
}

But I don't understand it… phpunit test suites can have test suites in their test suites? Can somebody explain?

EDIT: I got an explanation from Nicolas Grekas: apparently, the sub-test suite can be used when there are data providers. The use case of this code block is to inject ClockMock functions in the namespace before it is used.

@greg0ire
Copy link
Contributor

greg0ire commented Oct 24, 2019

Sebastian Bergmann wrote:

The concept of a "test suite" is one that should, and will, eventually vanish. Therefore there is no equivalent in the new API.

Yet test suite related methods are not marked as deprecated yet, so I think I can ignore this for the moment and just work on following the deprecations for the other methods.

EDIT: I noticed I need to do the something similar but simpler for CoverageTestListener, so I started working on a CoverageHook class… then I noticed the new signature: public function executeBeforeTest(string $test): void. So apparently we only have access to a string, no Test instance… not sure what to do with this string 🤦‍♂️

The main discussion happens here: sebastianbergmann/phpunit#3390

@nicolas-grekas
Copy link
Member Author

ping @epdenouden

@nicolas-grekas
Copy link
Member Author

For reference, the doc has some reminders about the features: https://symfony.com/doc/current/components/phpunit_bridge.html

@rbaarsma
Copy link
Contributor

Not sure if somebody made a PR already, but the solution is to use extensions (possible from phpunit >= 7.1) instead of listeners.

The CHANGELOG should reflect that the phpunit.xml.dist should be changed to use the new feature.

The code would be something like this:

class CoverageExtension implements BeforeTestHook
{
    public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false)
    {
        $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound);
    }

    public function executeBeforeTest(string $test): void
    {
        $this->trait->startTest($test);
    }
}

And the phpunit.xml.dist would be to remove the listener and instead add an extension:

    <extensions>
        <extension class="Symfony\Bridge\PhpUnit\Legacy\CoverageExtension" />
    </extensions>

Note that CoverageExtension is just one of the phpunit-bridge extensions. The same system applies to other existing extensions.

I could write the code, but don't want to interfere with existing work on this.

@greg0ire
Copy link
Contributor

@rbaarsma your solution will not work for the more complicated SymfonyTestListener I'm afraid, but you can still make a PR just for the CoverageListener I guess 👍

@rbaarsma
Copy link
Contributor

@greg0ire after some more examination and attempted solution, I come to the same conclusion as you already said before. The string $test given is not enough to cover the use-case. I assumed this was the test class, but it can be SelfDescribing and then be really only a string.

My conclusion is that the current hook implementation is not enough to migrate these classes. Perhaps we should contact @sebastianbergmann to discuss what his idea is with the current hook implementation and how we are supposed to use the minimum input to cover the use-cases of symfony, such as access to the annotations. testResultObject, status, and failures.

Also the SymfonyTestListener uses a globalListenerDisabled function that does not seem to have a hook.

@epdenouden
Copy link

epdenouden commented Nov 26, 2019

Hello everyone!
including @greg0ire @nicolas-grekas @sebastianbergmann @localheinz and @theseer

At the SymfonyCon in Amsterdam last weekend we decided on refactoring both in PHPUnit and the Symfony test collection, which will remove the need for a bridge altogether. PHPUnit will get new features that will bring much of the bridge functionality to its core, whereas any missing Symfony-specific requirements will be handled with a proper PHPUnit extension.

The upcoming event-based logging system in PHPUnit allows for much better logging and analysis of -you guessed it!- events happening during testing. Thanks to the work by @localheinz and @theseer we will be able to report many new events in much more detail.

@sebastianbergmann and I discussed this with @greg0ire and @nicolas-grekas last Saturday and I will write up a list of what is needed where and how to approach this.

Also, we propose a communal bonfire during a conference next year to burn all the legacy code.

@derrabus
Copy link
Member

derrabus commented Oct 21, 2020

On PHPUnit 9, our TestListener is broken.

Nevermind, that's actually the test listener of the polyfills package. I should've read the error message more carefully. 🙈

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Hello? This issue is about to be closed if nobody replies.

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help wanted Issues and PRs which are looking for volunteers to complete them. PhpUnitBridge Stalled
Projects
None yet
Development

No branches or pull requests

7 participants