From ca8d147f958c2ec6ec2f2ad05e5b28bd0b32193b Mon Sep 17 00:00:00 2001 From: sonali-dudhia Date: Mon, 13 Jul 2026 19:08:10 +0530 Subject: [PATCH] fix: run `pest()->afterAll()` hooks by flushing statics after `tearDownAfterClass()` Fixes #1694 Testable::tearDown()'s finally block flushed the afterAll static after every test via TestCaseMethodFactory::tearDown(), so by the time PHPUnit called tearDownAfterClass() (which runs after the last test's tearDown()) the hook had already been nulled out. The per-test flush was redundant: TestCaseMethodFactory::setUp() already flushes statics before re-applying proxies for the next test. Move the flush into tearDownAfterClass()'s finally block instead, so it happens after the hook actually runs. Co-Authored-By: Claude Sonnet 5 --- src/Concerns/Testable.php | 9 +++---- src/Factories/TestCaseMethodFactory.php | 8 ------- tests/Hooks/AfterAllTest.php | 31 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 tests/Hooks/AfterAllTest.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index a6f78d25b..14f8a4d52 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -232,7 +232,11 @@ public static function tearDownAfterClass(): void $afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll); } - call_user_func(Closure::bind($afterAll, null, self::class)); + try { + call_user_func(Closure::bind($afterAll, null, self::class)); + } finally { + self::flush(); + } parent::tearDownAfterClass(); } @@ -371,9 +375,6 @@ protected function tearDown(...$arguments): void parent::tearDown(); TestSuite::getInstance()->test = null; - - $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $method->tearDown($this); } } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index f0a734016..652799149 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -146,14 +146,6 @@ public function setUp(TestCase $concrete): void $this->factoryProxies->proxy($concrete); } - /** - * Flushes the test case. - */ - public function tearDown(TestCase $concrete): void - { - $concrete::flush(); // @phpstan-ignore-line - } - /** * Creates the test's closure. */ diff --git a/tests/Hooks/AfterAllTest.php b/tests/Hooks/AfterAllTest.php new file mode 100644 index 000000000..1282247dc --- /dev/null +++ b/tests/Hooks/AfterAllTest.php @@ -0,0 +1,31 @@ +calls->afterAllInFile = 0; +$_SERVER['globalHook']->calls->afterAllTestsRan = 0; + +pest()->afterAll(function () { + $_SERVER['globalHook']->calls->afterAllInFile++; +}); + +it('does not get called before all tests 1', function () { + $_SERVER['globalHook']->calls->afterAllTestsRan++; + expect($_SERVER['globalHook']->calls->afterAllInFile)->toBe(0); +})->repeat(2); + +it('does not get called before all tests 2', function () { + $_SERVER['globalHook']->calls->afterAllTestsRan++; + expect($_SERVER['globalHook']->calls->afterAllInFile)->toBe(0); +}); + +register_shutdown_function(function () { + // Under --parallel, Pest also evaluates this file in the parent process to + // build the suite; the tests (and therefore afterAll) only run in the worker. + // Skip the assertion in any process where no test of this file actually ran. + if ($_SERVER['globalHook']->calls->afterAllTestsRan === 0) { + return; + } + + assertSame(1, $_SERVER['globalHook']->calls->afterAllInFile, 'Expected the [pest()->afterAll()] hook to have been called exactly once.'); +});