Skip to content

[PhpUnitBridge] support ClockMock and DnsMock with PHPUnit 10+ #58467

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

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,11 @@ jobs:
mkdir -p /opt/php/lib
echo memory_limit=-1 > /opt/php/lib/php.ini
./build/php/bin/php ./phpunit --colors=always src/Symfony/Component/Process

- name: Run PhpUnitBridge tests with PHPUnit 11
if: '! matrix.mode'
run: |
./phpunit src/Symfony/Bridge/PhpUnit
env:
SYMFONY_PHPUNIT_VERSION: '11.3'
SYMFONY_DEPRECATIONS_HELPER: 'disabled'
1 change: 1 addition & 0 deletions src/Symfony/Bridge/PhpUnit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
7.2
---

* Add a PHPUnit extension that registers the clock mock and DNS mock and the `DebugClassLoader` from the ErrorHandler component if present
* Add `ExpectUserDeprecationMessageTrait` with a polyfill of PHPUnit's `expectUserDeprecationMessage()`
* Use `total` for asserting deprecation count when a group is not defined

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit\Extension;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Metadata\Group;
use Symfony\Bridge\PhpUnit\ClockMock;

/**
* @internal
*/
class DisableClockMockSubscriber implements FinishedSubscriber

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php:23:45: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\FinishedSubscriber does not exist (see https://psalm.dev/019)

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php:23:45: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\FinishedSubscriber does not exist (see https://psalm.dev/019)
{
public function notify(Finished $event): void
{
$test = $event->test();

if (!$test instanceof TestMethod) {
return;
}

foreach ($test->metadata() as $metadata) {
if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
ClockMock::withClockMock(false);
}
}
}
}
39 changes: 39 additions & 0 deletions src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit\Extension;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Metadata\Group;
use Symfony\Bridge\PhpUnit\DnsMock;

/**
* @internal
*/
class DisableDnsMockSubscriber implements FinishedSubscriber

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php:23:43: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\FinishedSubscriber does not exist (see https://psalm.dev/019)

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php:23:43: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\FinishedSubscriber does not exist (see https://psalm.dev/019)
{
public function notify(Finished $event): void
{
$test = $event->test();

if (!$test instanceof TestMethod) {
return;
}

foreach ($test->metadata() as $metadata) {
if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) {
DnsMock::withMockedHosts([]);
}
}
}
}
39 changes: 39 additions & 0 deletions src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit\Extension;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\PreparationStartedSubscriber;
use PHPUnit\Metadata\Group;
use Symfony\Bridge\PhpUnit\ClockMock;

/**
* @internal
*/
class EnableClockMockSubscriber implements PreparationStartedSubscriber

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php:23:44: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\PreparationStartedSubscriber does not exist (see https://psalm.dev/019)

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php:23:44: UndefinedClass: Class, interface or enum named PHPUnit\Event\Test\PreparationStartedSubscriber does not exist (see https://psalm.dev/019)
{
public function notify(PreparationStarted $event): void
{
$test = $event->test();

if (!$test instanceof TestMethod) {
return;
}

foreach ($test->metadata() as $metadata) {
if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
ClockMock::withClockMock(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SymfonyTestsListenerTrait also registers the mock on starting the test in addition to enabling it. Is is safe to drop that part ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this already happens in the RegisterClockMockSubscriber which is run on the test suite level, doing it here as well would just redo what already happened before

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit\Extension;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\TestSuite\Loaded;
use PHPUnit\Event\TestSuite\LoadedSubscriber;
use PHPUnit\Metadata\Group;
use Symfony\Bridge\PhpUnit\ClockMock;

/**
* @internal
*/
class RegisterClockMockSubscriber implements LoadedSubscriber

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php:23:46: UndefinedClass: Class, interface or enum named PHPUnit\Event\TestSuite\LoadedSubscriber does not exist (see https://psalm.dev/019)

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php:23:46: UndefinedClass: Class, interface or enum named PHPUnit\Event\TestSuite\LoadedSubscriber does not exist (see https://psalm.dev/019)
{
public function notify(Loaded $event): void
{
foreach ($event->testSuite()->tests() as $test) {
if (!$test instanceof TestMethod) {
continue;
}

foreach ($test->metadata() as $metadata) {
if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
ClockMock::register($test->className());
}
}
}
}
}
39 changes: 39 additions & 0 deletions src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit\Extension;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\TestSuite\Loaded;
use PHPUnit\Event\TestSuite\LoadedSubscriber;
use PHPUnit\Metadata\Group;
use Symfony\Bridge\PhpUnit\DnsMock;

/**
* @internal
*/
class RegisterDnsMockSubscriber implements LoadedSubscriber

Check failure on line 23 in src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php:23:44: UndefinedClass: Class, interface or enum named PHPUnit\Event\TestSuite\LoadedSubscriber does not exist (see https://psalm.dev/019)
{
public function notify(Loaded $event): void
{
foreach ($event->testSuite()->tests() as $test) {
if (!$test instanceof TestMethod) {
continue;
}

foreach ($test->metadata() as $metadata) {
if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) {
DnsMock::register($test->className());
}
}
}
}
}
52 changes: 52 additions & 0 deletions src/Symfony/Bridge/PhpUnit/SymfonyExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\PhpUnit;

use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
use Symfony\Bridge\PhpUnit\Extension\DisableClockMockSubscriber;
use Symfony\Bridge\PhpUnit\Extension\DisableDnsMockSubscriber;
use Symfony\Bridge\PhpUnit\Extension\EnableClockMockSubscriber;
use Symfony\Bridge\PhpUnit\Extension\RegisterClockMockSubscriber;
use Symfony\Bridge\PhpUnit\Extension\RegisterDnsMockSubscriber;
use Symfony\Component\ErrorHandler\DebugClassLoader;

class SymfonyExtension implements Extension

Check failure on line 25 in src/Symfony/Bridge/PhpUnit/SymfonyExtension.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Bridge/PhpUnit/SymfonyExtension.php:25:35: UndefinedClass: Class, interface or enum named PHPUnit\Runner\Extension\Extension does not exist (see https://psalm.dev/019)
{
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also needs to enable the DebugClassLoader if available, as done in SymfonyTestsListenerTrait.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not enable the DebugClassLoader here on purpose. PHPUnit defers the execution of bootstrapping extensions until the test suite to run was actually loaded. Thus, enabling the class loader here is too late to trigger deprecations for classes that have been loaded before. Right now, the only place to enable it early enough is a custom bootstrap file (like done in #58370).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But for projects that don't use a custom bootstrap file, I don't see which custom code can run before the loading of the testsuite. So the autoloading of custom classes will still probably happen after that point.
So the bootstrapping of extensions looks like a good place to register it for a better DX for the simple case (DebugClassLoader::enable is already handling the case where autoloaders have already been wrapped by a DebugClassLoader, so it is fine if both the bootstrap file and the extension bootstrapper call it)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough, this would still fail if a class (e.g. a fixture) is defined in the same file as a test, but it will probably still cover 99% of the other use cases

{
if (class_exists(DebugClassLoader::class)) {
DebugClassLoader::enable();
}

if ($parameters->has('clock-mock-namespaces')) {
foreach (explode(',', $parameters->get('clock-mock-namespaces')) as $namespace) {
ClockMock::register($namespace.'\DummyClass');
}
}

$facade->registerSubscriber(new RegisterClockMockSubscriber());
$facade->registerSubscriber(new EnableClockMockSubscriber());
$facade->registerSubscriber(new DisableClockMockSubscriber());

if ($parameters->has('dns-mock-namespaces')) {
foreach (explode(',', $parameters->get('dns-mock-namespaces')) as $namespace) {
DnsMock::register($namespace.'\DummyClass');
}
}

$facade->registerSubscriber(new RegisterDnsMockSubscriber());
$facade->registerSubscriber(new DisableDnsMockSubscriber());
}
}
3 changes: 3 additions & 0 deletions src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

use PHPUnit\Framework\TestCase;

/**
* @requires PHPUnit < 10
*/
class CoverageListenerTest extends TestCase
{
public function test()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ public function testExistingBaselineAndGeneration()
$this->assertEquals(json_encode($expected, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename));
}

/**
* @requires PHPUnit < 10
*/
public function testBaselineGenerationWithDeprecationTriggeredByDebugClassLoader()
{
$filename = $this->createFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
--TEST--
Test DeprecationErrorHandler with log file
--SKIPIF--
<?php if (!getenv('SYMFONY_PHPUNIT_VERSION') || version_compare(getenv('SYMFONY_PHPUNIT_VERSION'), '10.0', '>=')) die('Skipping on PHPUnit 10+');
--FILE--
<?php
$filename = tempnam(sys_get_temp_dir(), 'sf-');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;

/**
* @requires PHPUnit < 10
*/
final class ExpectDeprecationTraitTest extends TestCase
{
use ExpectDeprecationTrait;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

use PHPUnit\Framework\TestCase;

/**
* @requires PHPUnit < 10
*/
final class ExpectedDeprecationAnnotationTest extends TestCase
{
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*
* This class is deliberately suffixed with *TestFail.php so that it is ignored
* by PHPUnit. This test is designed to fail. See ../expectdeprecationfail.phpt.
*
* @requires PHPUnit < 10
*/
final class ExpectDeprecationTraitTestFail extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
/**
* This class is deliberately suffixed with *TestRisky.php so that it is ignored
* by PHPUnit. This test is designed to fail. See ../expectnotrisky.phpt.
*
* @requires PHPUnit < 10
*/
final class NoAssertionsTestNotRisky extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
/**
* This class is deliberately suffixed with *TestRisky.php so that it is ignored
* by PHPUnit. This test is designed to fail. See ../expectrisky.phpt.
*
* @requires PHPUnit < 10
*/
final class NoAssertionsTestRisky extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
failOnRisky="true"
failOnWarning="true"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="Fixtures/symfonyextension Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>

<source ignoreSuppressionOfDeprecations="true">
<include>
<directory>src</directory>
</include>
</source>

<extensions>
<bootstrap class="Symfony\Bridge\PhpUnit\SymfonyExtension">
<parameter name="clock-mock-namespaces" value="App" />
<parameter name="dns-mock-namespaces" value="App" />
</bootstrap>
</extensions>
</phpunit>
Loading