From 8b2f19e31ea0b5e7500fb164c7a22a0064e225af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Mon, 10 Mar 2025 18:36:49 +0000 Subject: [PATCH 1/3] feat: Clean-up listener/subscriber after the test Fixes #74 --- classes/MockDisablerPHPUnit10.php | 15 +++++++- classes/MockDisablerPHPUnit6.php | 12 ++++++- classes/MockDisablerPHPUnit7.php | 12 ++++++- classes/PHPMock.php | 31 ++++++++++++++-- tests/MockDisablerTest.php | 19 ++++++++++ tests/PHPMockTest.php | 60 +++++++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 5 deletions(-) diff --git a/classes/MockDisablerPHPUnit10.php b/classes/MockDisablerPHPUnit10.php index be536b0..1f096b8 100644 --- a/classes/MockDisablerPHPUnit10.php +++ b/classes/MockDisablerPHPUnit10.php @@ -22,15 +22,22 @@ class MockDisablerPHPUnit10 implements FinishedSubscriber * @var Deactivatable The function mocks. */ private $deactivatable; + + /** + * @var callable|null The callback to execute after the test. + */ + private $callback; /** * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. + * @param callback|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable) + public function __construct(Deactivatable $deactivatable, ?callable $callback = null) { $this->deactivatable = $deactivatable; + $this->callback = $callback; } /** @@ -39,10 +46,16 @@ public function __construct(Deactivatable $deactivatable) public function notify(Finished $event) : void { $this->deactivatable->disable(); + if ($this->callback !== null) { + call_user_func($this->callback, $this); + } } public function endTest(): void { $this->deactivatable->disable(); + if ($this->callback !== null) { + call_user_func($this->callback, $this); + } } } diff --git a/classes/MockDisablerPHPUnit6.php b/classes/MockDisablerPHPUnit6.php index 3db2fcc..2c98995 100644 --- a/classes/MockDisablerPHPUnit6.php +++ b/classes/MockDisablerPHPUnit6.php @@ -22,15 +22,22 @@ class MockDisablerPHPUnit6 extends BaseTestListener * @var Deactivatable The function mocks. */ private $deactivatable; + + /** + * @var callable|null The callback to execute after the test. + */ + private $callback; /** * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. + * @param callable|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable) + public function __construct(Deactivatable $deactivatable, callable $callback = null) { $this->deactivatable = $deactivatable; + $this->callback = $callback; } /** @@ -46,5 +53,8 @@ public function endTest(Test $test, $time) parent::endTest($test, $time); $this->deactivatable->disable(); + if ($this->callback !== null) { + call_user_func($this->callback, $this); + } } } diff --git a/classes/MockDisablerPHPUnit7.php b/classes/MockDisablerPHPUnit7.php index 44c5141..59aa7e7 100644 --- a/classes/MockDisablerPHPUnit7.php +++ b/classes/MockDisablerPHPUnit7.php @@ -22,15 +22,22 @@ class MockDisablerPHPUnit7 extends BaseTestListener * @var Deactivatable The function mocks. */ private $deactivatable; + + /** + * @var callable|null The callback to execute after the test. + */ + private $callback; /** * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. + * @param callable|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable) + public function __construct(Deactivatable $deactivatable, $callback = null) { $this->deactivatable = $deactivatable; + $this->callback = $callback; } /** @@ -46,5 +53,8 @@ public function endTest(Test $test, float $time) : void parent::endTest($test, $time); $this->deactivatable->disable(); + if ($this->callback !== null) { + call_user_func($this->callback, $this); + } } } diff --git a/classes/PHPMock.php b/classes/PHPMock.php index 4575504..c672285 100644 --- a/classes/PHPMock.php +++ b/classes/PHPMock.php @@ -9,6 +9,7 @@ use PHPUnit\Event\Facade; use PHPUnit\Framework\MockObject\MockObject; use ReflectionClass; +use ReflectionMethod; use ReflectionProperty; use SebastianBergmann\Template\Template; @@ -117,8 +118,32 @@ public function registerForTearDown(Deactivatable $deactivatable) $property->setAccessible(true); $property->setValue($facade, false); + $method = new ReflectionMethod($facade, 'deferredDispatcher'); + $method->setAccessible(true); + $dispatcher = $method->invoke($facade); + + $propDispatcher = new ReflectionProperty($dispatcher, 'dispatcher'); + $propDispatcher->setAccessible(true); + $directDispatcher = $propDispatcher->getValue($dispatcher); + + $propSubscribers = new ReflectionProperty($directDispatcher, 'subscribers'); + $propSubscribers->setAccessible(true); + $facade->registerSubscriber( - new MockDisabler($deactivatable) + new MockDisabler( + $deactivatable, + static function (MockDisabler $original) use ($directDispatcher, $propSubscribers) { + $subscribers = $propSubscribers->getValue($directDispatcher); + + foreach ($subscribers['PHPUnit\Event\Test\Finished'] as $key => $subscriber) { + if ($original === $subscriber) { + unset($subscribers['PHPUnit\Event\Test\Finished'][$key]); + } + } + + $propSubscribers->setValue($directDispatcher, $subscribers); + } + ) ); $property->setValue($facade, true); @@ -127,7 +152,9 @@ public function registerForTearDown(Deactivatable $deactivatable) } $result = $this->getTestResultObject(); - $result->addListener(new MockDisabler($deactivatable)); + $result->addListener(new MockDisabler($deactivatable, static function (MockDisabler $listener) use ($result) { + $result->removeListener($listener); + })); } /** diff --git a/tests/MockDisablerTest.php b/tests/MockDisablerTest.php index ba7a805..4dbadca 100644 --- a/tests/MockDisablerTest.php +++ b/tests/MockDisablerTest.php @@ -2,6 +2,7 @@ namespace phpmock\phpunit; +use phpmock\Deactivatable; use phpmock\Mock; use PHPUnit\Framework\TestCase; @@ -32,4 +33,22 @@ public function testEndTest() $this->assertEquals(1, min(1, 9)); } + + public function testCallback() + { + $executed = false; + $executedWith = null; + $mock = $this->createMock(Deactivatable::class); + $disabler = new MockDisabler($mock, static function ($disabler) use (&$executed, &$executedWith) { + self::assertInstanceOf(MockDisabler::class, $disabler); + + $executed = true; + $executedWith = $disabler; + }); + + $disabler->endTest($this, 1); + + self::assertTrue($executed); + self::assertSame($executedWith, $disabler); + } } diff --git a/tests/PHPMockTest.php b/tests/PHPMockTest.php index da26159..582b1c7 100644 --- a/tests/PHPMockTest.php +++ b/tests/PHPMockTest.php @@ -3,6 +3,7 @@ namespace phpmock\phpunit; use phpmock\AbstractMockTestCase; +use phpmock\Deactivatable; use PHPUnit\Framework\ExpectationFailedException; /** @@ -63,4 +64,63 @@ public function testFunctionMockFailsExpectation() time(); // satisfy the expectation } } + + /** + * Register a Deactivatable for a tear down. + * + * @test + */ + public function testRegisterForTearDownRegistered() + { + $obj = new \stdClass(); + $obj->count = 0; + + $class = new class ($obj) implements Deactivatable + { + private $obj; + + public function __construct($obj) + { + $this->obj = $obj; + } + + public function disable() + { + ++$this->obj->count; + } + }; + $this->registerForTearDown($class); + + self::assertSame(0, $obj->count); + + return $obj; + } + + /** + * Check the Deactivatable was executed on a tear down of dependent test. + * + * @test + * + * @depends testRegisterForTearDownRegistered + */ + #[\PHPUnit\Framework\Attributes\Depends('testRegisterForTearDownRegistered')] + public function testRegisterForTearDownExecuted($obj) + { + self::assertSame(1, $obj->count); + + return $obj; + } + + /** + * Check the Deactivatable was unregistered after executing, so it is not executed again. + * + * @test + * + * @depends testRegisterForTearDownExecuted + */ + #[\PHPUnit\Framework\Attributes\Depends('testRegisterForTearDownExecuted')] + public function testRegisterForTearDownRemoved($obj) + { + self::assertSame(1, $obj->count); + } } From 2f52f1b2a4032efc26d2e23ad52d8488e08501cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Mon, 10 Mar 2025 19:00:19 +0000 Subject: [PATCH 2/3] use Closure instead of callback and drop call_user_func - per @mvorisek --- classes/MockDisablerPHPUnit10.php | 11 ++++++----- classes/MockDisablerPHPUnit6.php | 9 +++++---- classes/MockDisablerPHPUnit7.php | 9 +++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/classes/MockDisablerPHPUnit10.php b/classes/MockDisablerPHPUnit10.php index 1f096b8..853e387 100644 --- a/classes/MockDisablerPHPUnit10.php +++ b/classes/MockDisablerPHPUnit10.php @@ -2,6 +2,7 @@ namespace phpmock\phpunit; +use Closure; use phpmock\Deactivatable; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; @@ -24,7 +25,7 @@ class MockDisablerPHPUnit10 implements FinishedSubscriber private $deactivatable; /** - * @var callable|null The callback to execute after the test. + * @var Closure|null The callback to execute after the test. */ private $callback; @@ -32,9 +33,9 @@ class MockDisablerPHPUnit10 implements FinishedSubscriber * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. - * @param callback|null $callback The callback to execute after the test. + * @param Closure|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable, ?callable $callback = null) + public function __construct(Deactivatable $deactivatable, ?Closure $callback = null) { $this->deactivatable = $deactivatable; $this->callback = $callback; @@ -47,7 +48,7 @@ public function notify(Finished $event) : void { $this->deactivatable->disable(); if ($this->callback !== null) { - call_user_func($this->callback, $this); + ($this->callback)($this); } } @@ -55,7 +56,7 @@ public function endTest(): void { $this->deactivatable->disable(); if ($this->callback !== null) { - call_user_func($this->callback, $this); + ($this->callback)($this); } } } diff --git a/classes/MockDisablerPHPUnit6.php b/classes/MockDisablerPHPUnit6.php index 2c98995..c2926ab 100644 --- a/classes/MockDisablerPHPUnit6.php +++ b/classes/MockDisablerPHPUnit6.php @@ -2,6 +2,7 @@ namespace phpmock\phpunit; +use Closure; use phpmock\Deactivatable; use PHPUnit\Framework\BaseTestListener; use PHPUnit\Framework\Test; @@ -24,7 +25,7 @@ class MockDisablerPHPUnit6 extends BaseTestListener private $deactivatable; /** - * @var callable|null The callback to execute after the test. + * @var Closure|null The callback to execute after the test. */ private $callback; @@ -32,9 +33,9 @@ class MockDisablerPHPUnit6 extends BaseTestListener * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. - * @param callable|null $callback The callback to execute after the test. + * @param Closure|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable, callable $callback = null) + public function __construct(Deactivatable $deactivatable, Closure $callback = null) { $this->deactivatable = $deactivatable; $this->callback = $callback; @@ -54,7 +55,7 @@ public function endTest(Test $test, $time) $this->deactivatable->disable(); if ($this->callback !== null) { - call_user_func($this->callback, $this); + ($this->callback)($this); } } } diff --git a/classes/MockDisablerPHPUnit7.php b/classes/MockDisablerPHPUnit7.php index 59aa7e7..41def45 100644 --- a/classes/MockDisablerPHPUnit7.php +++ b/classes/MockDisablerPHPUnit7.php @@ -2,6 +2,7 @@ namespace phpmock\phpunit; +use Closure; use phpmock\Deactivatable; use PHPUnit\Framework\BaseTestListener; use PHPUnit\Framework\Test; @@ -24,7 +25,7 @@ class MockDisablerPHPUnit7 extends BaseTestListener private $deactivatable; /** - * @var callable|null The callback to execute after the test. + * @var Closure|null The callback to execute after the test. */ private $callback; @@ -32,9 +33,9 @@ class MockDisablerPHPUnit7 extends BaseTestListener * Sets the function mocks. * * @param Deactivatable $deactivatable The function mocks. - * @param callable|null $callback The callback to execute after the test. + * @param Closure|null $callback The callback to execute after the test. */ - public function __construct(Deactivatable $deactivatable, $callback = null) + public function __construct(Deactivatable $deactivatable, Closure $callback = null) { $this->deactivatable = $deactivatable; $this->callback = $callback; @@ -54,7 +55,7 @@ public function endTest(Test $test, float $time) : void $this->deactivatable->disable(); if ($this->callback !== null) { - call_user_func($this->callback, $this); + ($this->callback)($this); } } } From a24da64708eb393af15ffabd77dd6e6894e48951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Wed, 12 Mar 2025 15:04:12 +0000 Subject: [PATCH 3/3] import PHPUnit\Event\Test\Finished and use ::class notation - per @mvorisek --- classes/PHPMock.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/PHPMock.php b/classes/PHPMock.php index c672285..c75a9d3 100644 --- a/classes/PHPMock.php +++ b/classes/PHPMock.php @@ -7,6 +7,7 @@ use phpmock\MockBuilder; use phpmock\Deactivatable; use PHPUnit\Event\Facade; +use PHPUnit\Event\Test\Finished; use PHPUnit\Framework\MockObject\MockObject; use ReflectionClass; use ReflectionMethod; @@ -135,9 +136,9 @@ public function registerForTearDown(Deactivatable $deactivatable) static function (MockDisabler $original) use ($directDispatcher, $propSubscribers) { $subscribers = $propSubscribers->getValue($directDispatcher); - foreach ($subscribers['PHPUnit\Event\Test\Finished'] as $key => $subscriber) { + foreach ($subscribers[Finished::class] as $key => $subscriber) { if ($original === $subscriber) { - unset($subscribers['PHPUnit\Event\Test\Finished'][$key]); + unset($subscribers[Finished::class][$key]); } }