diff --git a/src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php b/src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php index e6d256528..2b82c54b6 100644 --- a/src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php +++ b/src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php @@ -14,6 +14,13 @@ use function Hypervel\Coroutine\run; /** + * Wraps each test method in a Swoole coroutine so that database connections, + * channels, and other coroutine-dependent APIs work correctly during tests. + * + * PHPUnit 10.5 made runTest() private, so we can no longer override it. + * Instead, we swap the test method name during setUp() — which runs before + * PHPUnit's private runTest() calls $this->{$this->name}(). + * * @method string name() */ trait RunTestsInCoroutine @@ -24,9 +31,22 @@ trait RunTestsInCoroutine protected string $realTestName = ''; + /** + * Swap the test method name so PHPUnit's private runTest() calls + * runTestsInCoroutine() instead of the real test method. + * The real test method is then executed inside a Swoole coroutine. + */ + protected function setUpCoroutineTest(): void + { + if (Coroutine::getCid() === -1 && $this->enableCoroutine) { + $this->realTestName = $this->name(); + $this->setName('runTestsInCoroutine'); + } + } + final protected function runTestsInCoroutine(...$arguments) { - parent::setName($this->realTestName); + $this->setName($this->realTestName); $testResult = null; $exception = null; @@ -56,16 +76,6 @@ final protected function runTestsInCoroutine(...$arguments) return $testResult; } - final protected function runTest(): mixed - { - if (Coroutine::getCid() === -1 && $this->enableCoroutine) { - $this->realTestName = $this->name(); - parent::setName('runTestsInCoroutine'); - } - - return parent::runTest(); - } - protected function invokeSetupInCoroutine(): void { if (method_exists($this, 'setUpInCoroutine')) { diff --git a/src/foundation/src/Testing/TestCase.php b/src/foundation/src/Testing/TestCase.php index 24f8af7df..b05671348 100644 --- a/src/foundation/src/Testing/TestCase.php +++ b/src/foundation/src/Testing/TestCase.php @@ -70,6 +70,15 @@ protected function setUp(): void } $this->setUpHasRun = true; + + // Swap the test method name for coroutine wrapping. + // This must happen AFTER setUp completes but BEFORE PHPUnit's + // private runTest() calls $this->{$this->name}(). + // PHPUnit 10.5 made runTest() private, so we can no longer + // override it — the name swap in setUp() is the only hook point. + if (method_exists($this, 'setUpCoroutineTest')) { + $this->setUpCoroutineTest(); + } } /** diff --git a/tests/Foundation/Testing/Concerns/RunTestsInCoroutineTest.php b/tests/Foundation/Testing/Concerns/RunTestsInCoroutineTest.php new file mode 100644 index 000000000..29a20de16 --- /dev/null +++ b/tests/Foundation/Testing/Concerns/RunTestsInCoroutineTest.php @@ -0,0 +1,150 @@ +assertSame(-1, Coroutine::getCid()); + $this->assertSame('myTestMethod', $testCase->name()); + + $method = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $method->invoke($testCase); + + $this->assertSame('runTestsInCoroutine', $testCase->name()); + + $realName = new ReflectionProperty($testCase, 'realTestName'); + $this->assertSame('myTestMethod', $realName->getValue($testCase)); + } + + public function testSetUpCoroutineTestDoesNotSwapWhenCoroutineDisabled() + { + $testCase = new CoroutineDisabledTestStub('myTestMethod'); + + $method = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $method->invoke($testCase); + + $this->assertSame('myTestMethod', $testCase->name()); + } + + public function testSetUpCoroutineTestIsNoOpInsideCoroutine() + { + $testCase = new CoroutineTestStub('myTestMethod'); + + \Hypervel\Coroutine\run(function () use ($testCase) { + $this->assertGreaterThan(-1, Coroutine::getCid()); + + $method = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $method->invoke($testCase); + + $this->assertSame('myTestMethod', $testCase->name()); + }); + } + + public function testRunTestsInCoroutineExecutesInCoroutine() + { + $testCase = new CoroutineTestStub('myTestMethod'); + + $setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $setUp->invoke($testCase); + + $run = new ReflectionMethod($testCase, 'runTestsInCoroutine'); + $run->invoke($testCase); + + $this->assertTrue($testCase->executedInCoroutine); + } + + public function testRunTestsInCoroutineRestoresOriginalName() + { + $testCase = new CoroutineTestStub('myTestMethod'); + + $setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $setUp->invoke($testCase); + + $this->assertSame('runTestsInCoroutine', $testCase->name()); + + $run = new ReflectionMethod($testCase, 'runTestsInCoroutine'); + $run->invoke($testCase); + + $this->assertSame('myTestMethod', $testCase->name()); + } + + public function testRunTestsInCoroutinePropagatesExceptions() + { + $testCase = new CoroutineExceptionTestStub('throwingMethod'); + + $setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest'); + $setUp->invoke($testCase); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Test exception from coroutine'); + + $run = new ReflectionMethod($testCase, 'runTestsInCoroutine'); + $run->invoke($testCase); + } +} + +/** + * @internal + * @coversNothing + */ +class CoroutineTestStub extends \PHPUnit\Framework\TestCase +{ + use RunTestsInCoroutine; + + public bool $executedInCoroutine = false; + + public function myTestMethod(): void + { + $this->executedInCoroutine = Coroutine::getCid() > -1; + } +} + +/** + * @internal + * @coversNothing + */ +class CoroutineDisabledTestStub extends \PHPUnit\Framework\TestCase +{ + use RunTestsInCoroutine; + + public function __construct(string $name) + { + parent::__construct($name); + $this->enableCoroutine = false; + } + + public function myTestMethod(): void + { + } +} + +/** + * @internal + * @coversNothing + */ +class CoroutineExceptionTestStub extends \PHPUnit\Framework\TestCase +{ + use RunTestsInCoroutine; + + public function throwingMethod(): void + { + throw new RuntimeException('Test exception from coroutine'); + } +}