Skip to content

fix: run pest()->afterAll() hooks by flushing statics after tearDownAfterClass()#1753

Open
sonalidudhia wants to merge 1 commit into
pestphp:4.xfrom
sonalidudhia:fix/afterall-hooks
Open

fix: run pest()->afterAll() hooks by flushing statics after tearDownAfterClass()#1753
sonalidudhia wants to merge 1 commit into
pestphp:4.xfrom
sonalidudhia:fix/afterall-hooks

Conversation

@sonalidudhia

@sonalidudhia sonalidudhia commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

fix: run pest()->afterAll() hooks by flushing statics after tearDownAfterClass()

Fixes #1694

What was happening

Hooks registered via pest()->afterAll() silently never executed. The reporter
observed the hook closure showing up as a NullClosure inside
tearDownAfterClass().

Root cause

Testable::tearDown()'s finally block called $method->tearDown($this)TestCaseMethodFactory::tearDown()$concrete::flush(), which nulls the static self::$__afterAll after
every test. PHPUnit calls tearDownAfterClass() after the last test's
tearDown(), so by the time it runs, the static holding the afterAll hook
had already been wiped — leaving the NullClosure the reporter saw.

beforeAll doesn't have this problem because statics are populated at
instance initialization, which happens before setUpBeforeClass() ever
consumes them — there's no intervening per-test flush in that direction.

The per-test flush was also redundant: TestCaseMethodFactory::setUp()
already flushes statics before re-applying the Uses-level hook proxies for
the next test, which is what actually prevents hooks from chaining
duplicates across tests, datasets, and repetitions.

The fix

  • Remove the redundant per-test flush (TestCaseMethodFactory::tearDown()
    and its call site in Testable::tearDown()) — setUp()'s pre-proxy flush
    already covers hygiene between tests.
  • Move the flush into tearDownAfterClass()'s finally block, after the
    afterAll hook chain runs, so the static resets at the correct point even
    if a hook throws. Hook ordering (uses-hooks chained before the file-level
    afterAll, then parent::tearDownAfterClass()) is unchanged.

Tests

tests/Hooks/ had BeforeAllTest.php, BeforeEachTest.php, and
AfterEachTest.php, but no AfterAllTest.php — that gap is why this
regression went unnoticed. Added tests/Hooks/AfterAllTest.php.

A conventional in-test expect() assertion can't verify this hook actually
ran, because afterAll only executes after every test in the class has
finished — there's no later test to observe it from. The test instead mirrors
the shutdown-assertion pattern used in tests/Features/TestCycle.php:
register_shutdown_function() asserts the hook fired exactly once, after the
whole process (including tearDownAfterClass()) has completed.

Evidence

On 4.x HEAD (test only, source unpatched):

   PASS  Tests\Hooks\AfterAllTest
  ✓ it does not get called before all tests 1 @ repetition 1 of 2        0.01s
  ✓ it does not get called before all tests 1 @ repetition 2 of 2
  ✓ it does not get called before all tests 2

   PASS  Tests\Hooks\AfterEachTest
  ✓ nested → nested afterEach execution order                            0.06s
  ✓ global afterEach execution order

   PASS  Tests\Hooks\BeforeAllTest
  ✓ it gets called before all tests 1 @ repetition 1 of 2                0.03s
  ✓ it gets called before all tests 1 @ repetition 2 of 2
  ✓ it gets called before all tests 2

   PASS  Tests\Hooks\BeforeEachTest
  ✓ global beforeEach execution order                                    0.06s

  Tests:    9 passed (84 assertions)
  Duration: 0.94s

EXIT: 255

Pest's own reporter shows green (the per-test assertions genuinely pass — the
bug is that afterAll never runs, so there's nothing in-test to fail on).
The process still exits 255: PHPUnit's register_shutdown_function
assertion fails after the runner has already printed its summary, because
afterAllInFile stayed 0 — the hook was never invoked.

With this fix:

   PASS  Tests\Hooks\AfterAllTest
  ✓ it does not get called before all tests 1 @ repetition 1 of 2        0.01s
  ✓ it does not get called before all tests 1 @ repetition 2 of 2
  ✓ it does not get called before all tests 2

   PASS  Tests\Hooks\AfterEachTest
  ✓ nested → nested afterEach execution order                            0.03s
  ✓ global afterEach execution order

   PASS  Tests\Hooks\BeforeAllTest
  ✓ it gets called before all tests 1 @ repetition 1 of 2                0.01s
  ✓ it gets called before all tests 1 @ repetition 2 of 2
  ✓ it gets called before all tests 2

   PASS  Tests\Hooks\BeforeEachTest
  ✓ global beforeEach execution order                                    0.01s

  Tests:    9 passed (84 assertions)
  Duration: 0.22s

EXIT: 0

As a side effect, the two pest()->in('Hooks')->afterAll() hooks declared in
tests/Pest.php (which target every file in this directory, including the
new one) now actually execute for the first time — their internal
expect() assertions pass under this fix.

…wnAfterClass()`

Fixes pestphp#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 <noreply@anthropic.com>
@sonalidudhia
sonalidudhia marked this pull request as ready for review July 15, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: AfterAll() hook not being called

1 participant