From a611e8afb52debc53b3cc99046f57c00bf71b5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Sun, 26 May 2024 14:02:25 -0400 Subject: [PATCH] [TesterBundle] PHPUnit SetUpAutowire extension refactoring and support AutowireParameter --- app/src/Repository/TagRepository.php | 2 +- .../SetUpAutowire/AutowireClient.php | 42 +++- .../SetUpAutowire/AutowireInterface.php | 12 ++ .../Extension/SetUpAutowire/AutowireMock.php | 43 +++- .../SetUpAutowire/AutowireMockProperty.php | 24 ++- .../SetUpAutowire/AutowireParameter.php | 37 ++++ .../SetUpAutowire/AutowireService.php | 35 +++- .../SetUpAutowire/SetUpAutowireExtension.php | 191 ++---------------- packages/tester-bundle/extension.neon | 9 +- .../SetUpAutowireExtensionTest.php | 92 +++++++++ 10 files changed, 298 insertions(+), 189 deletions(-) create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireParameter.php create mode 100644 tests/TesterBundle/PHPUnit/Extension/SetUpAutoWire/SetUpAutowireExtensionTest.php diff --git a/app/src/Repository/TagRepository.php b/app/src/Repository/TagRepository.php index 3f24b8c54..08f4f8b6d 100644 --- a/app/src/Repository/TagRepository.php +++ b/app/src/Repository/TagRepository.php @@ -13,7 +13,7 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Tag::class); } - public function findActive() + public function findActive(): array { return $this->findBy(['active' => true]); } diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireClient.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireClient.php index 2fe25ab5d..db016f5bf 100644 --- a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireClient.php +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireClient.php @@ -4,9 +4,19 @@ namespace Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire; +use Draw\Bundle\TesterBundle\WebTestCase as DrawWebTestCase; +use Draw\Component\Core\Reflection\ReflectionAccessor; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as SymfonyWebTestCase; + #[\Attribute(\Attribute::TARGET_PROPERTY)] -class AutowireClient +class AutowireClient implements AutowireInterface { + public static function getPriority(): int + { + return 1000; + } + public function __construct( private array $options = [], private array $server = [], @@ -22,4 +32,34 @@ public function getServer(): array { return $this->server; } + + public function autowire(TestCase $testCase, \ReflectionProperty $reflectionProperty): void + { + if (!$testCase instanceof SymfonyWebTestCase && !$testCase instanceof DrawWebTestCase) { + throw new \RuntimeException( + sprintf( + 'AutowireClient attribute can only be used in %s or %s.', + SymfonyWebTestCase::class, + DrawWebTestCase::class + ) + ); + } + + // This is to ensure the kernel is not booted before calling createClient + // Can happen if we use the container in a setUpBeforeClass method or a beforeClass hook + ReflectionAccessor::callMethod( + $testCase, + 'ensureKernelShutdown' + ); + + $reflectionProperty->setValue( + $testCase, + ReflectionAccessor::callMethod( + $testCase, + 'createClient', + $this->getOptions(), + $this->getServer() + ) + ); + } } diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php new file mode 100644 index 000000000..ff164ada9 --- /dev/null +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php @@ -0,0 +1,12 @@ +getName(); + $type = $reflectionProperty->getType(); + + if (!$type instanceof \ReflectionIntersectionType) { + throw new \RuntimeException('Property '.$propertyName.' of class '.$testCase::class.' must have a type hint intersection with Mock.'); + } + + $types = $type->getTypes(); + + if (2 !== \count($types)) { + throw new \RuntimeException('Property '.$propertyName.' of class '.$testCase::class.' can only have 2 intersection types.'); + } + + foreach ($types as $type) { + if (!$type instanceof \ReflectionNamedType) { + throw new \RuntimeException('Property '.$propertyName.' of class '.$testCase::class.' intersection must be of named type.'); + } + + if (MockObject::class === $type->getName()) { + continue; + } + + $reflectionProperty->setValue( + $testCase, + ReflectionAccessor::callMethod($testCase, 'createMock', $type->getName()) + ); + + return; + } + } } diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireMockProperty.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireMockProperty.php index 7a37d3348..a12692003 100644 --- a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireMockProperty.php +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireMockProperty.php @@ -2,9 +2,17 @@ namespace Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire; +use Draw\Component\Core\Reflection\ReflectionAccessor; +use PHPUnit\Framework\TestCase; + #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] -class AutowireMockProperty +class AutowireMockProperty implements AutowireInterface { + public static function getPriority(): int + { + return -100; + } + public function __construct(private string $property, private ?string $fromProperty = null) { $this->fromProperty ??= $property; @@ -19,4 +27,18 @@ public function getFromProperty(): string { return $this->fromProperty; } + + public function autowire(TestCase $testCase, \ReflectionProperty $reflectionProperty): void + { + $object = $reflectionProperty->getValue($testCase); + + ReflectionAccessor::setPropertyValue( + $object, + $this->getProperty(), + ReflectionAccessor::getPropertyValue( + $testCase, + $this->getFromProperty() + ) + ); + } } diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireParameter.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireParameter.php new file mode 100644 index 000000000..bdf376764 --- /dev/null +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireParameter.php @@ -0,0 +1,37 @@ +parameter; + } + + public function autowire(TestCase $testCase, \ReflectionProperty $reflectionProperty): void + { + \assert($testCase instanceof KernelTestCase); + + $container = (new \ReflectionMethod($testCase, 'getContainer'))->invoke($testCase); + + $reflectionProperty->setValue( + $testCase, + $container->get(ParameterBagInterface::class)->resolveValue($this->getParameter()) + ); + } +} diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireService.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireService.php index 1b48b49ef..cc1e0a661 100644 --- a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireService.php +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireService.php @@ -2,9 +2,18 @@ namespace Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire; +use Draw\Component\Core\Reflection\ReflectionExtractor; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + #[\Attribute(\Attribute::TARGET_PROPERTY)] -class AutowireService +class AutowireService implements AutowireInterface { + public static function getPriority(): int + { + return 0; + } + public function __construct(private ?string $serviceId = null) { } @@ -13,4 +22,28 @@ public function getServiceId(): ?string { return $this->serviceId; } + + public function autowire(TestCase $testCase, \ReflectionProperty $reflectionProperty): void + { + \assert($testCase instanceof KernelTestCase); + + $serviceId = $this->serviceId; + + if (null === $serviceId) { + $classes = ReflectionExtractor::getClasses($reflectionProperty->getType()); + + if (1 !== \count($classes)) { + throw new \RuntimeException('Property '.$reflectionProperty->getName().' of class '.$testCase::class.' must have a type hint.'); + } + + $serviceId = $classes[0]; + } + + $container = (new \ReflectionMethod($testCase, 'getContainer'))->invoke($testCase); + + $reflectionProperty->setValue( + $testCase, + $container->get($serviceId) + ); + } } diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/SetUpAutowireExtension.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/SetUpAutowireExtension.php index f71f1c216..06effae02 100644 --- a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/SetUpAutowireExtension.php +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/SetUpAutowireExtension.php @@ -2,20 +2,14 @@ namespace Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire; -use Draw\Bundle\TesterBundle\WebTestCase as DrawWebTestCase; -use Draw\Component\Core\Reflection\ReflectionAccessor; -use Draw\Component\Core\Reflection\ReflectionExtractor; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Test\Prepared as TestPrepared; use PHPUnit\Event\Test\PreparedSubscriber as TestPreparedSubscriber; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as SymfonyWebTestCase; class SetUpAutowireExtension implements Extension { @@ -24,15 +18,10 @@ public function bootstrap(Configuration $configuration, Facade $facade, Paramete $facade->registerSubscribers( new class() implements TestPreparedSubscriber { /** - * @var array> + * @var array> */ private array $propertyAttributes = []; - /** - * @var array> - */ - private array $propertyMocks = []; - public function notify(TestPrepared $event): void { $test = $event->test(); @@ -56,43 +45,10 @@ public function notify(TestPrepared $event): void return; } - if (!$testCase instanceof KernelTestCase) { - return; - } - - $this->initializeClients($testCase); - - $container = null; - - foreach ($this->getPropertyAttributes($testCase) as [$property, $serviceId]) { - $container ??= ReflectionAccessor::callMethod($testCase, 'getContainer'); + foreach ($this->getPropertyAttributes($testCase) as [$property, $autowire]) { + \assert($autowire instanceof AutowireInterface); - if ($serviceId instanceof AutowireMock) { - $property->setValue( - $testCase, - $this->getMockFor($testCase, $property->getName()) - ); - - continue; - } - - $property->setValue( - $testCase, - $container->get($serviceId) - ); - } - - foreach ($this->getPropertyMockAttributes($testCase) as [$property, $autoWireMockProperty]) { - $service = $property->getValue($testCase); - - ReflectionAccessor::setPropertyValue( - $service, - $autoWireMockProperty->getProperty(), - ReflectionAccessor::getPropertyValue( - $testCase, - $autoWireMockProperty->getFromProperty() - ) - ); + $autowire->autowire($testCase, $property); } if ($testCase instanceof AutowiredCompletionAwareInterface) { @@ -100,155 +56,38 @@ public function notify(TestPrepared $event): void } } - private function initializeClients(TestCase $testCase): void - { - $clientAttributes = iterator_to_array($this->getClientAttributes($testCase)); - - if (empty($clientAttributes)) { - return; - } - - if (!$testCase instanceof SymfonyWebTestCase && !$testCase instanceof DrawWebTestCase) { - throw new \RuntimeException( - sprintf( - 'AutowireClient attribute can only be used in %s or %s.', - SymfonyWebTestCase::class, - DrawWebTestCase::class - ) - ); - } - - // This is to ensure the kernel is not booted before calling createClient - // Can happen if we use the container in a setUpBeforeClass method or a beforeClass hook - ReflectionAccessor::callMethod( - $testCase, - 'ensureKernelShutdown' - ); - - foreach ($clientAttributes as [$property, $attribute]) { - \assert($property instanceof \ReflectionProperty); - \assert($attribute instanceof \ReflectionAttribute); - - $autoWireClient = $attribute->newInstance(); - \assert($autoWireClient instanceof AutowireClient); - - $property->setValue( - $testCase, - ReflectionAccessor::callMethod( - $testCase, - 'createClient', - $autoWireClient->getOptions(), - $autoWireClient->getServer() - ) - ); - } - } - - private function getClientAttributes(TestCase $testCase): iterable - { - foreach ((new \ReflectionObject($testCase))->getProperties() as $property) { - $attribute = $property->getAttributes( - AutowireClient::class, - \ReflectionAttribute::IS_INSTANCEOF - )[0] ?? null; - - if (null !== $attribute) { - yield [$property, $attribute]; - } - } - } - /** - * @return iterable + * @return iterable */ private function getPropertyAttributes(TestCase $testCase): iterable { $className = $testCase::class; if (!\array_key_exists($className, $this->propertyAttributes)) { - $this->propertyAttributes[$className] = []; + $autowireAttributes = []; foreach ((new \ReflectionObject($testCase))->getProperties() as $property) { - $attribute = $property->getAttributes(AutowireService::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null; - - if (!$attribute) { - continue; - } + foreach ($property->getAttributes() as $attribute) { + $attributeClass = $attribute->getName(); - $autoWireService = $attribute->newInstance(); - - if ($autoWireService instanceof AutowireMock) { - $this->propertyAttributes[$className][] = [$property, $autoWireService]; - - continue; - } - - $serviceId = $autoWireService->getServiceId(); - - if (!$serviceId) { - $classes = ReflectionExtractor::getClasses($property->getType()); - if (1 !== \count($classes)) { - throw new \RuntimeException('Property '.$property->getName().' of class '.$testCase::class.' must have a type hint.'); + if (!(new \ReflectionClass($attributeClass))->implementsInterface(AutowireInterface::class)) { + continue; } - $serviceId = $classes[0]; + $autowireAttributes[] = [$property, $attribute->newInstance()]; } + } - $this->propertyAttributes[$className][] = [$property, $serviceId]; + usort($autowireAttributes, static fn ($a, $b) => $a[1]::getPriority() <=> $b[1]::getPriority()); - foreach ($property->getAttributes(AutowireMockProperty::class) as $attribute) { - $autoWireMockProperty = $attribute->newInstance(); - $this->propertyMocks[$className][] = [$property, $autoWireMockProperty]; - } - } + // We reverse because priority 1 comes before priority 0 + $this->propertyAttributes[$className] = array_reverse($autowireAttributes); } foreach ($this->propertyAttributes[$className] as $property) { yield $property; } } - - /** - * @return iterable - */ - private function getPropertyMockAttributes(TestCase $testCase): iterable - { - yield from $this->propertyMocks[$testCase::class] ?? []; - } - - private function getMockFor(TestCase $testCase, string $property) - { - $reflectionProperty = new \ReflectionProperty($testCase, $property); - - $type = $reflectionProperty->getType(); - - if (!$type instanceof \ReflectionIntersectionType) { - throw new \RuntimeException('Property '.$property.' of class '.$testCase::class.' must have a type hint intersection with Mock.'); - } - - $types = $type->getTypes(); - - if (2 !== \count($types)) { - throw new \RuntimeException('Property '.$property.' of class '.$testCase::class.' can only have 2 intersection types.'); - } - - foreach ($types as $type) { - if (!$type instanceof \ReflectionNamedType) { - throw new \RuntimeException('Property '.$property.' of class '.$testCase::class.' intersction must be of named type.'); - } - - if (MockObject::class === $type->getName()) { - continue; - } - - $reflectionProperty->setValue( - $testCase, - $mock = ReflectionAccessor::callMethod($testCase, 'createMock', $type->getName()) - ); - - return $mock; - } - } }, ); } diff --git a/packages/tester-bundle/extension.neon b/packages/tester-bundle/extension.neon index 902f9e69c..74a17fffe 100644 --- a/packages/tester-bundle/extension.neon +++ b/packages/tester-bundle/extension.neon @@ -2,13 +2,6 @@ services: draw.tester_bundle.autowire_client_read_write_properties_extension: class: Draw\Bundle\TesterBundle\PHPStan\Rules\Properties\AutowireReadWritePropertiesExtension arguments: - - 'Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireClient' - tags: - - phpstan.properties.readWriteExtension - - draw.tester_bundle.autowire_service_read_write_properties_extension: - class: Draw\Bundle\TesterBundle\PHPStan\Rules\Properties\AutowireReadWritePropertiesExtension - arguments: - - 'Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireService' + - 'Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireInterface' tags: - phpstan.properties.readWriteExtension diff --git a/tests/TesterBundle/PHPUnit/Extension/SetUpAutoWire/SetUpAutowireExtensionTest.php b/tests/TesterBundle/PHPUnit/Extension/SetUpAutoWire/SetUpAutowireExtensionTest.php new file mode 100644 index 000000000..57a748d43 --- /dev/null +++ b/tests/TesterBundle/PHPUnit/Extension/SetUpAutoWire/SetUpAutowireExtensionTest.php @@ -0,0 +1,92 @@ +get(UserSetCommentNullMigration::class), + $this->userSetCommentNullMigration + ); + } + + public function testAutowiredClient(): void + { + static::assertSame( + static::getClient(), + $this->client + ); + } + + public function testAutowiredMock(): void + { + static::assertInstanceOf( + ManagerRegistry::class, + $this->managerRegistry + ); + + static::assertInstanceOf( + MockObject::class, + $this->managerRegistry + ); + } + + public function testAutowireMockProperty(): void + { + static::assertSame( + (new \ReflectionProperty($this->userSetCommentNullMigration, 'managerRegistry')) + ->getValue($this->userSetCommentNullMigration), + $this->managerRegistry + ); + } + + public function testAutowiredTransportTester(): void + { + static::assertSame( + static::getContainer()->get('messenger.transport.sync.draw.tester'), + $this->transportTester + ); + } + + public function testAutowiredParameter(): void + { + static::assertSame( + 'test_resolved', + $this->parameter + ); + } +}