From 139c61602c4e14897e2275aa86b6d8f996b0eb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Fri, 16 Feb 2024 19:09:48 -0500 Subject: [PATCH] [TesterBundle] phpunit test injection --- .../AutowireReadWritePropertiesExtension.php | 32 ++++++ .../SetUpAutowire/AutowireInterface.php | 7 ++ .../SetUpAutowire/AutowireService.php | 16 +++ .../SetUpAutowire/AutowireTransportTester.php | 12 ++ .../SetUpAutowire/SetUpAutowireExtension.php | 108 ++++++++++++++++++ packages/tester-bundle/extension.neon | 5 + phpstan.neon | 1 + phpunit.xml.dist | 1 + tests/Entity/UserTest.php | 27 ++--- .../ValueIsNotUsedValidatorTest.php | 10 +- 10 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 packages/tester-bundle/PHPStan/Rules/Properties/AutowireReadWritePropertiesExtension.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireService.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireTransportTester.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/SetUpAutowireExtension.php create mode 100644 packages/tester-bundle/extension.neon diff --git a/packages/tester-bundle/PHPStan/Rules/Properties/AutowireReadWritePropertiesExtension.php b/packages/tester-bundle/PHPStan/Rules/Properties/AutowireReadWritePropertiesExtension.php new file mode 100644 index 000000000..e8ed50a4b --- /dev/null +++ b/packages/tester-bundle/PHPStan/Rules/Properties/AutowireReadWritePropertiesExtension.php @@ -0,0 +1,32 @@ +hasProperAttribute($property, $propertyName, AutowireService::class); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->hasProperAttribute($property, $propertyName, AutowireService::class); + } + + private function hasProperAttribute(PropertyReflection $property, string $propertyName, string $attribute): bool + { + $properReflection = $property->getDeclaringClass()->getNativeProperty($propertyName)->getNativeReflection(); + + return 0 !== \count($properReflection->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)); + } +} 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..b2880dca5 --- /dev/null +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireInterface.php @@ -0,0 +1,7 @@ +serviceId; + } +} diff --git a/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireTransportTester.php b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireTransportTester.php new file mode 100644 index 000000000..301b48603 --- /dev/null +++ b/packages/tester-bundle/PHPUnit/Extension/SetUpAutowire/AutowireTransportTester.php @@ -0,0 +1,12 @@ +registerSubscribers( + new class() implements TestPreparedSubscriber { + /** + * @var array> + */ + private array $propertyAttributes = []; + + public function notify(TestPrepared $event): void + { + $test = $event->test(); + + \assert($test instanceof TestMethod); + + if (!is_a($test->className(), AutowireInterface::class, true)) { + return; + } + + $testCase = null; + + foreach (debug_backtrace() as $frame) { + if (isset($frame['object']) && $frame['object'] instanceof TestCase) { + $testCase = $frame['object']; + break; + } + } + + if (!$testCase instanceof AutowireInterface) { + return; + } + + if (!$testCase instanceof KernelTestCase) { + return; + } + + $container = null; + + foreach ($this->getPropertyAttributes($testCase) as [$property, $serviceId]) { + $container ??= ReflectionAccessor::callMethod($testCase, 'getContainer'); + + $property->setValue( + $testCase, + $container->get($serviceId) + ); + } + } + + /** + * @return iterable + */ + private function getPropertyAttributes(TestCase $testCase): iterable + { + $className = $testCase::class; + + if (!\array_key_exists($className, $this->propertyAttributes)) { + $this->propertyAttributes[$className] = []; + + foreach ((new \ReflectionObject($testCase))->getProperties() as $property) { + $attribute = $property->getAttributes(AutowireService::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null; + + if (!$attribute) { + continue; + } + + $autoWireService = $attribute->newInstance(); + + $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.'); + } + + $serviceId = $classes[0]; + } + + $this->propertyAttributes[$className][] = [$property, $serviceId]; + } + } + + foreach ($this->propertyAttributes[$className] as $property) { + yield $property; + } + } + }, + ); + } +} diff --git a/packages/tester-bundle/extension.neon b/packages/tester-bundle/extension.neon new file mode 100644 index 000000000..0ef6a5e9b --- /dev/null +++ b/packages/tester-bundle/extension.neon @@ -0,0 +1,5 @@ +services: + draw.tester_bundle.autowire_read_write_properties_extension: + class: Draw\Bundle\TesterBundle\PHPStan\Rules\Properties\AutowireReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/phpstan.neon b/phpstan.neon index 0e50e2a1f..7621ed6ef 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - phpstan-baseline.neon + - packages/tester-bundle/extension.neon parameters: level: 5 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 24fe8f256..13cd60fc8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -41,5 +41,6 @@ + diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index 86dd56e87..1d558bb66 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -6,33 +6,32 @@ use App\Tests\TestCase; use Doctrine\ORM\EntityManagerInterface; use Draw\Bundle\TesterBundle\Messenger\TransportTester; +use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireInterface; +use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireService; +use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireTransportTester; use Draw\Bundle\UserBundle\Entity\UserLock; use Draw\Bundle\UserBundle\Message\NewUserLockMessage; use Draw\Bundle\UserBundle\Message\UserLockDelayedActivationMessage; use Draw\Component\Messenger\Searchable\Stamp\SearchableTagStamp; +use PHPUnit\Framework\Attributes\After; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\ReceivedStamp; -class UserTest extends TestCase +class UserTest extends TestCase implements AutowireInterface { + #[AutowireService] private EntityManagerInterface $entityManager; + #[AutowireTransportTester('sync')] private TransportTester $transportTester; - protected function setUp(): void - { - $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); - $this->transportTester = static::getTransportTester('sync'); - } + #[AutowireService] + private MessageBusInterface $messageBus; - /** - * @before - * - * @after - */ + #[After] public function cleanUp(): void { - static::getService(EntityManagerInterface::class) + $this->entityManager ->createQueryBuilder() ->delete(User::class, 'user') ->where('user.email = :email') @@ -72,9 +71,7 @@ public function testLockDelayed(): void static::assertInstanceOf(NewUserLockMessage::class, $envelope->getMessage()); $this->transportTester->reset(); - static::getContainer() - ->get(MessageBusInterface::class) - ->dispatch($envelope->with(new ReceivedStamp('sync'))); + $this->messageBus->dispatch($envelope->with(new ReceivedStamp('sync'))); $this->transportTester->assertMessageMatch(UserLockDelayedActivationMessage::class); diff --git a/tests/Validator/Constraints/ValueIsNotUsedValidatorTest.php b/tests/Validator/Constraints/ValueIsNotUsedValidatorTest.php index 30ed74b40..c7b841e76 100644 --- a/tests/Validator/Constraints/ValueIsNotUsedValidatorTest.php +++ b/tests/Validator/Constraints/ValueIsNotUsedValidatorTest.php @@ -4,19 +4,23 @@ use App\Entity\Tag; use App\Entity\User; +use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireInterface; +use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireService; use Draw\Component\Validator\Constraints\ValueIsNotUsed; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Validator\Validator\ValidatorInterface; -class ValueIsNotUsedValidatorTest extends KernelTestCase +class ValueIsNotUsedValidatorTest extends KernelTestCase implements AutowireInterface { + #[AutowireService] + protected ValidatorInterface $validator; + /** * @dataProvider provideTestValidate */ public function testValidate(mixed $value, string $entityClass, string $field, bool $expectError): void { - $violations = static::getContainer() - ->get(ValidatorInterface::class) + $violations = $this->validator ->validate($value, new ValueIsNotUsed(entityClass: $entityClass, field: $field)); if (!$expectError) {