Skip to content

Test collection silently fails on PHPUnit 12 / Pest 4 — $app is already null when Passed/Failed/Skipped events fire #761

Description

@likeadeckofcards

Thank you for all of the work you have done on Clockwork. I recently ran into an issue getting data while running pest tests. Below is a summary from Claude as to why I am not seeing anything.

============

Environment

  • itsgoingd/clockwork v5.3.5
  • phpunit/phpunit 12.5.33
  • pestphp/pest v4
  • laravel/framework v12
  • PHP 8.3.32

Config

  • config/clockwork.php: tests.collect → true (via CLOCKWORK_TESTS_COLLECT=true)
  • CLOCKWORK_ENABLE=true
  • phpunit.xml has the extension registered correctly:

Symptom
No test metadata is ever written to storage/clockwork when running tests via php artisan test, vendor/bin/pest, or vendor/bin/phpunit, despite tests.collect being enabled and no errors being reported.

Root cause (confirmed via temporary debug logging in ClockworkExtension.php)

  1. ClockworkExtension::bootstrap() runs, and subscribers are registered correctly.
  2. PassedSubscriber/FailedSubscriber/etc. do fire recordTest() for every test.
  3. Inside recordTest(), resolveApp($testCase) returns null on every single test, so the method returns early before ever calling isCollectingTests() or storeRequest().
  4. resolveApp() (in Clockwork/Support/Laravel/Tests/ClockworkExtension.php) reflects on the test case's $app property:
    protected static function resolveApp($testCase)
    {
    $reflectionClass = new \ReflectionClass($testCase);
    if ($reflectionClass->hasProperty('app')) {
    $reflectionProperty = $reflectionClass->getProperty('app');
    $reflectionProperty->setAccessible(true);
    if ($reflectionProperty->getValue($testCase)) {
    return $reflectionProperty->getValue($testCase);
    }
    } elseif (method_exists($testCase, 'createApplication')) {
    return $testCase->createApplication();
    }
    }
  5. Laravel's Illuminate\Foundation\Testing\Concerns\InteractsWithTestCaseLifecycle::tearDownTheTestEnvironment() sets $this->app = null inside tearDown().
  6. In PHPUnit 12's event system, tearDown() runs before the Test\Passed (and Failed/Skipped/Errored) event is emitted (see vendor/phpunit/phpunit/src/Framework/TestCase.php around lines 621–659 — fixture teardown happens, then $emitter->testPassed(...) is dispatched).
  7. So by the time Clockwork's subscriber runs resolveApp(), $app has already been nulled by Laravel's own teardown, and the property check at line 102 (if ($reflectionProperty->getValue($testCase))) fails, causing a silent no-op with no error or log output.

Why this looks new: the CHANGELOG for 5.3.5 lists support for "PHPUnit 11 and Pest 3." This suggests the event-timing relative to tearDown() changed (or was already like this but tolerated differently) by PHPUnit 12, breaking the $app-via-reflection approach for PHPUnit 12 / Pest 4.

Suggested fix direction

  • Capture the app instance earlier in the test lifecycle (e.g. via a PreparedSubscriber or Event\Test\Started hook, before tearDown() runs) and cache it per-test, rather than reflecting on the (possibly torn-down) test case object inside Passed/Failed/Skipped handlers.
  • Alternatively, fall back to Illuminate\Foundation\Application::getInstance() if the reflected $app property is null, if the static instance is still available at that point.

Repro: any Laravel 12 + Pest 4 + PHPUnit 12 project with itsgoingd/clockwork 5.3.5, tests.collect enabled, and the extension registered in phpunit.xml — no test metadata files are ever created in storage/clockwork, and no error/warning is surfaced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions