diff --git a/.env b/.env index 03264939b..4033ed180 100644 --- a/.env +++ b/.env @@ -11,3 +11,6 @@ USE_DAMA_DOCTRINE_TEST_BUNDLE="0" USE_FOUNDRY_PHPUNIT_EXTENSION="0" USE_PHP_84_LAZY_OBJECTS="0" PHPUNIT_VERSION="12" # allowed values: 9, 10, 11, 12 + +# Only relevant for "reset-database" testsuite +DATABASE_RESET_MODE="schema" # allowed values: schema, migrate diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 5ba91e373..60d7581ef 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -125,7 +125,7 @@ jobs: shell: bash test-reset-database: - name: Reset DB - D:${{ matrix.database }} ${{ matrix.use-dama == 1 && ' (dama)' || '' }} ${{ matrix.reset-database-mode == 'migrate' && ' (migrate)' || '' }} ${{ contains(matrix.with-migration-configuration-file, 'transactional') && '(configuration file transactional)' || contains(matrix.with-migration-configuration-file, 'configuration') && '(configuration file)' || '' }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }} + name: Reset DB - D:${{ matrix.database }} ${{ matrix.use-dama == 1 && ' (dama)' || '' }} ${{ matrix.reset-database-mode == 'migrate' && ' (migrate)' || '' }} ${{ contains(matrix.with-migration-configuration-file, 'transactional') && '(configuration file transactional)' || contains(matrix.with-migration-configuration-file, 'configuration') && '(configuration file)' || '' }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-phpunit-extension == 0 && ' (no phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -135,17 +135,21 @@ jobs: reset-database-mode: [ schema, migrate ] migration-configuration-file: ['no'] deps: [ highest, lowest ] + use-phpunit-extension: [ 1 ] include: - - { database: mongo, migration-configuration-file: 'no', use-dama: 0, reset-database-mode: schema } - - { database: pgsql, migration-configuration-file: 'migration-configuration', use-dama: 0, reset-database-mode: migration } - - { database: pgsql, migration-configuration-file: 'migration-configuration-transactional', use-dama: 0, reset-database-mode: migration } + - { database: mongo, use-dama: 0, reset-database-mode: schema, use-phpunit-extension: 1 } + - { database: pgsql, use-dama: 1, reset-database-mode: schema, use-phpunit-extension: 0 } + - { database: pgsql, use-dama: 0, reset-database-mode: schema, use-phpunit-extension: 0 } + - { database: pgsql, migration-configuration-file: 'migration-configuration', use-dama: 0, reset-database-mode: migration, use-phpunit-extension: 1 } + - { database: pgsql, migration-configuration-file: 'migration-configuration-transactional', use-dama: 0, reset-database-mode: migration, use-phpunit-extension: 1 } env: DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || 'sqlite:///%kernel.project_dir%/var/data.db' }} MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && 1 || 0 }} DATABASE_RESET_MODE: ${{ matrix.reset-database-mode == 1 && 1 || 0 }} MIGRATION_CONFIGURATION_FILE: ${{ matrix.migration-configuration-file == 'no' && '' || format('tests/Fixture/MigrationTests/configs/{0}.php', matrix.migration-configuration-file) }} - PHPUNIT_VERSION: 11 + PHPUNIT_VERSION: 12 + USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension }} services: postgres: image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} @@ -202,7 +206,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ 8.1, 8.2, 8.3, 8.4 ] + php: [ 8.2, 8.3, 8.4 ] steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/README.md b/README.md index e66a91662..434e5b137 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ $ composer update # run main testsuite (with "schema" reset database strategy) $ ./phpunit -# run "migrate" testsuite (with "migrate" reset database strategy) +# run "reset-database" testsuite $ ./phpunit --testsuite reset-database ``` @@ -73,6 +73,7 @@ PHPUNIT_VERSION="11" # possible values: 9, 10, 11, 11.4 # test reset database with migrations, # only relevant for "reset-database" testsuite +DATABASE_RESET_MODE="migrate" MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php" # run test suite with postgreSQL diff --git a/UPGRADE-2.10.md b/UPGRADE-2.10.md new file mode 100644 index 000000000..566e7150b --- /dev/null +++ b/UPGRADE-2.10.md @@ -0,0 +1,53 @@ +# Migration guide from Foundry 2.9 to 2.10 + +The main feature of Foundry 2.10 is the deprecation of the `ResetDatabase` trait, in favor of a `#[ResetDatabase]` attribute, +along with the [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension) +shipped by Foundry. + +The trait will be removed in Foundry 3.0, and the usage of the attribute will be mandatory to reset the database in your tests. + +> [!WARNING] +> The PHPUnit extension mechanism was introduced in PHPUnit 10. This means that Foundry 3 won't be compatible +> with PHPUnit 9 anymore (but Foundry 2 will remain compatible with PHPUnit 9). + +## How to + +> [!IMPORTANT] +> If you're still not using PHPUnit 10 or grater, there is nothing to do (yet!) + +Enable Foundry's [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension) +in your `phpunit.xml` file: + +```xml + + + + + +``` + +And then, replace all the `use ResetDatabase;` statements by a `#[\Zenstruck\Foundry\Attribute\ResetDatabase]` attribute +on your test classes. Note that you can put the attribute on a parent class, it will be inherited by all its children. + +## Rector rules + +A Rector set is available to automatically replace the trait by the attribute in all your tests. + +First, you'll need to install `rector/rector`: +```shell +composer require --dev rector/rector +``` + +Then, create a `rector.php` file: + +```php +withPaths(['tests']) + ->withSets([FoundrySetList::FOUNDRY_2_10]) +; +``` diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md index 88bfc072a..2a5937c55 100644 --- a/UPGRADE-2.7.md +++ b/UPGRADE-2.7.md @@ -52,7 +52,7 @@ return RectorConfig::configure() 'src', 'tests' ]) - ->withSets([FoundrySetList::REMOVE_PROXIES]) + ->withSets([FoundrySetList::FOUNDRY_2_7]) ; ``` diff --git a/UPGRADE-2.9.md b/UPGRADE-2.9.md new file mode 100644 index 000000000..94fdae932 --- /dev/null +++ b/UPGRADE-2.9.md @@ -0,0 +1,53 @@ +# Migration guide from Foundry 2.8 to 2.9 + +The main feature of Foundry 2.9 is the deprecation of the `Factories` trait, in favor of the [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension) +shipped by Foundry. It was necessary to remember to add the trait in every test class. And in some cases, Foundry could +still work even if the trait wasn’t added to the test, which could lead to subtle bugs. Now, Foundry is globally enabled +once for all. + +The trait will be removed in Foundry 3.0, and the extension will be mandatory. + +> [!WARNING] +> The PHPUnit extension mechanism was introduced in PHPUnit 10. This means that Foundry 3 won't be compatible +> with PHPUnit 9 anymore (but Foundry 2 will remain compatible with PHPUnit 9). + +## How to + +> [!IMPORTANT] +> If you're still not using PHPUnit 10 or grater, there is nothing to do (yet!) + +Enable Foundry's [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension) +in your `phpunit.xml` file: + +```xml + + + + + +``` + +And then, remove all the `use Factories;` statements from your factories. + +## Rector rules + +A Rector set is available to automatically remove the usage of the trait in all your tests. + +First, you'll need to install `rector/rector`: +```shell +composer require --dev rector/rector +``` + +Then, create a `rector.php` file: + +```php +withPaths(['tests']) + ->withSets([FoundrySetList::FOUNDRY_2_9]) +; +``` diff --git a/docs/index.rst b/docs/index.rst index 9897437f4..1952d4e87 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1683,59 +1683,90 @@ Let's look at an example: .. _enable-foundry-in-your-testcase: -Enable Foundry in your TestCase -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Globally Enable Foundry In PHPUnit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the ``Factories`` trait for tests using factories: +Add Foundry's `PHPUnit Extension`_ in your `phpunit.xml` file: -:: +.. configuration-block:: - use App\Factory\PostFactory; - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Zenstruck\Foundry\Test\Factories; + .. code-block:: xml - class MyTest extends WebTestCase - { - use Factories; + + + + + + +.. versionadded:: 2.9 + + The ability to globally enable Foundry with PHPUnit extension was introduced in Foundry 2.9 and requires at least + PHPUnit 10. + +.. note:: + + If you're still using PHPUnit 9, Foundry can be enabled by adding the trait ``Zenstruck\Foundry\Test\Factories`` + in each test:: + + use App\Factory\PostFactory; + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Zenstruck\Foundry\Test\Factories; - public function test_1(): void + class MyTest extends WebTestCase { - $post = PostFactory::createOne(); + use Factories; - // ... + public function test_something(): void + { + $post = PostFactory::createOne(); + + // ... + } } - } Database Reset ~~~~~~~~~~~~~~ -This library requires that your database be reset before each test. The packaged ``ResetDatabase`` trait handles +This library requires that your database be reset before each test. The packaged ``ResetDatabase`` attribute handles this for you. :: use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Zenstruck\Foundry\Test\Factories; - use Zenstruck\Foundry\Test\ResetDatabase; + use Zenstruck\Foundry\Attribute\ResetDatabase; + #{ResetDatabase] class MyTest extends WebTestCase { - use ResetDatabase, Factories; - // ... } -Before the first test using the ``ResetDatabase`` trait, it drops (if exists) and creates the test database. +Before the first test using the ``ResetDatabase`` attribute, it drops (if exists) and creates the test database. Then, by default, before each test, it resets the schema using ``doctrine:schema:drop``/``doctrine:schema:create``. +.. note:: + + If you're still using PHPUnit 9, the database can be reset by adding the trait ``Zenstruck\Foundry\Test\ResetDatabase``:: + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Zenstruck\Foundry\Test\Factories; + use Zenstruck\Foundry\Test\ResetDatabase; + + class MyTest extends WebTestCase + { + use ResetDatabase, Factories; + + // ... + } + .. tip:: - Create a base TestCase for tests using factories to avoid adding the traits to every TestCase. + Create a base TestCase for tests using factories to avoid adding the attribute to every TestCase. .. tip:: If your tests :ref:`are not persisting ` the objects they create, the ``ResetDatabase`` - trait is not required. + attribute is not required. By default, ``ResetDatabase`` resets the default configured connection's database and default configured object manager's schema. To customize the connection's and object manager's to be reset (or reset multiple connections/managers), use the @@ -1857,7 +1888,7 @@ Foundry provides a mechanism to automatically refresh inside a functional test t class MyTest extends WebTestCase { - use Factories, ResetDatabase; + use ResetDatabase; public function test_with_autorefresh(): void { @@ -2107,7 +2138,7 @@ Global State If you have an initial database state you want for all tests, you can set this in the config of the bundle. Accepted values are: stories as service, "global" stories and invokable services. Global state is loaded before each test using -the ``ResetDatabase`` trait. If you are using `DamaDoctrineTestBundle`_, it is only loaded once for the entire +the ``ResetDatabase`` attribute. If you are using `DamaDoctrineTestBundle`_, it is only loaded once for the entire test suite. .. configuration-block:: @@ -2130,7 +2161,7 @@ test suite. .. note:: - The :ref:`ResetDatabase ` trait is required when using global state. + The :ref:`ResetDatabase ` attribute is required when using global state. .. warning:: @@ -2326,7 +2357,7 @@ This library integrates seamlessly with `DAMADoctrineTestBundle withoutPersisting()`` is not necessary). Because the bundle is not available in these tests, -any bundle configuration you have will not be picked up. +``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``). These tests still require enabling Foundry with the PHPUnit extension +(or using the ``Factories`` trait if you still use PHPUnit 9) to boot Foundry but will not have doctrine available. +Factories created in these tests will not be persisted (calling ``->withoutPersisting()`` is not necessary). Because +the bundle is not available in these tests, any bundle configuration you have will not be picked up. :: @@ -2455,8 +2486,6 @@ any bundle configuration you have will not be picked up. class MyUnitTest extends TestCase { - use Factories; - public function some_test(): void { $post = PostFactory::createOne(); @@ -2673,19 +2702,19 @@ Full Default Bundle Configuration orm: reset: - # DBAL connections to reset with ResetDatabase trait + # DBAL connections to reset with ResetDatabase attribute connections: # Default: - default - # Entity Managers to reset with ResetDatabase trait + # Entity Managers to reset with ResetDatabase attribute entity_managers: # Default: - default - # Reset mode to use with ResetDatabase trait + # Reset mode to use with ResetDatabase attribute mode: schema # One of "schema"; "migrate" migrations: @@ -2695,7 +2724,7 @@ Full Default Bundle Configuration mongo: reset: - # Document Managers to reset with ResetDatabase trait + # Document Managers to reset with ResetDatabase attribute document_managers: # Default: diff --git a/phpstan.neon b/phpstan.neon index 9ac64d6fa..124e3db45 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -30,10 +30,6 @@ parameters: - identifier: missingType.iterableValue path: tests/ - # We support both PHPUnit versions (this method changed in PHPUnit 10) - - identifier: function.impossibleType - path: src/Test/Factories.php - # PHPStan does not understand PHP version checks - message: '#Comparison operation "(<|>|<=|>=)" between int<80\d+, 80\d+> and 80\d+ is always (false|true).#' diff --git a/phpunit-deprecation-baseline.xml b/phpunit-deprecation-baseline.xml index 4b2706232..3c09a2fef 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -7,13 +7,21 @@ + + + - + + + diff --git a/src/Attribute/ResetDatabase.php b/src/Attribute/ResetDatabase.php new file mode 100644 index 000000000..ccde895c8 --- /dev/null +++ b/src/Attribute/ResetDatabase.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS)] +final class ResetDatabase +{ +} diff --git a/src/Configuration.php b/src/Configuration.php index b21b6836a..76dab41a0 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -21,6 +21,7 @@ use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistedObjectsTracker; use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond @@ -129,7 +130,9 @@ public static function instance(): self throw new FoundryNotBooted(); } - FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait(); + if (!FoundryExtension::isEnabled()) { + FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait(); + } return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; } @@ -143,6 +146,10 @@ public static function isBooted(): bool public static function boot(\Closure|self $configuration): void { self::$instance = $configuration; + + if (FoundryExtension::shouldBeEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.9', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.9.md to upgrade.'); + } } /** @param \Closure():self|self $configuration */ diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php index 5f7fbef27..353058bae 100644 --- a/src/Exception/FoundryNotBooted.php +++ b/src/Exception/FoundryNotBooted.php @@ -11,6 +11,8 @@ namespace Zenstruck\Foundry\Exception; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; + /** * @author Kevin Bond */ @@ -18,6 +20,10 @@ final class FoundryNotBooted extends \LogicException { public function __construct() { - parent::__construct('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + $message = FoundryExtension::shouldBeEnabled() + ? 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure Foundry\'s PHPUnit extension is enabled.' + : 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'; + + parent::__construct($message); } } diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index e6a1e47d4..8da7d9d5c 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -125,6 +125,10 @@ public static function range(Factory $factory, int $min, int $max): self throw new \InvalidArgumentException('Min must be less than max.'); } + if ($factory instanceof PersistentObjectFactory && $factory->isPersisting() && Configuration::instance()->inADataProvider()) { + throw new \InvalidArgumentException('Using randomized "range" factory in data provider is not supported.'); + } + return new self($factory, static fn() => \array_fill(0, \mt_rand($min, $max), [])); } diff --git a/src/PHPUnit/AttributeReader.php b/src/PHPUnit/AttributeReader.php new file mode 100644 index 000000000..34da8c67f --- /dev/null +++ b/src/PHPUnit/AttributeReader.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class AttributeReader +{ + private function __construct() + { + } + + /** + * @template T of object + * + * @param class-string $attributeClass + * + * @return list<\ReflectionAttribute> + */ + public static function collectAttributesFromClassAndParents(string $attributeClass, \ReflectionClass $class): array // @phpstan-ignore missingType.generics + { + return [ + ...$class->getAttributes($attributeClass), + ...( + $class->getParentClass() + ? self::collectAttributesFromClassAndParents($attributeClass, $class->getParentClass()) + : [] + ), + ]; + } +} diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php deleted file mode 100644 index 8f58ca6bb..000000000 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\PHPUnit; - -use PHPUnit\Event; -use Zenstruck\Foundry\Configuration; -use Zenstruck\Foundry\InMemory\AsInMemoryTest; - -/** - * @internal - * @author Nicolas PHILIPPE - */ -final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber -{ - public function notify(Event\Test\DataProviderMethodCalled $event): void - { - if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { - $event->testMethod()->className()::_bootForDataProvider(); - } - - $testMethod = $event->testMethod(); - - if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) { - Configuration::instance()->enableInMemory(); - } - } -} diff --git a/src/PHPUnit/BootFoundryOnPreparationStarted.php b/src/PHPUnit/BootFoundryOnPreparationStarted.php new file mode 100644 index 000000000..aaea4a092 --- /dev/null +++ b/src/PHPUnit/BootFoundryOnPreparationStarted.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Test\UnitTestConfig; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BootFoundryOnPreparationStarted implements Event\Test\PreparationStartedSubscriber +{ + public function notify(Event\Test\PreparationStarted $event): void + { + $test = $event->test(); + + if (!$test->isTestMethod()) { + return; + } + /** @var Event\Code\TestMethod $test */ + $this->bootFoundry($test->className()); + } + + /** + * @param class-string $className + */ + private function bootFoundry(string $className): void + { + if (!\is_subclass_of($className, TestCase::class)) { + return; + } + + // unit test + if (!\is_subclass_of($className, KernelTestCase::class)) { + Configuration::boot(UnitTestConfig::build()); + + return; + } + + // integration test + Configuration::boot(static function() use ($className): Configuration { + if (!KernelTestCaseHelper::getContainer($className)->has('.zenstruck_foundry.configuration')) { + throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); + } + + return KernelTestCaseHelper::getContainer($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type + }); + } +} diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php index ff3ea9eb4..e68c6dc5d 100644 --- a/src/PHPUnit/BuildStoryOnTestPrepared.php +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -16,7 +16,6 @@ use PHPUnit\Event; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; -use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; /** * @internal @@ -35,7 +34,7 @@ public function notify(Event\Test\Prepared $event): void /** @var Event\Code\TestMethod $test */ $reflectionClass = new \ReflectionClass($test->className()); $withStoryAttributes = [ - ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass), + ...AttributeReader::collectAttributesFromClassAndParents(WithStory::class, $reflectionClass), ...$reflectionClass->getMethod($test->methodName())->getAttributes(WithStory::class), ]; @@ -47,25 +46,8 @@ public function notify(Event\Test\Prepared $event): void throw new \InvalidArgumentException(\sprintf('The test class "%s" must extend "%s" to use the "%s" attribute.', $test->className(), KernelTestCase::class, WithStory::class)); } - FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className()); - foreach ($withStoryAttributes as $withStoryAttribute) { $withStoryAttribute->newInstance()->story::load(); } } - - /** - * @return list<\ReflectionAttribute> - */ - private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass $class): array // @phpstan-ignore missingType.generics - { - return [ - ...$class->getAttributes(WithStory::class), - ...( - $class->getParentClass() - ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass()) - : [] - ), - ]; - } } diff --git a/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php new file mode 100644 index 000000000..1f4e6e4cc --- /dev/null +++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit\DataProvider; + +use PHPUnit\Event; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper; +use Zenstruck\Foundry\Test\UnitTestConfig; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber +{ + public function notify(Event\Test\DataProviderMethodCalled $event): void + { + $this->bootFoundryForDataProvider($event->testMethod()->className()); + + $testMethod = $event->testMethod(); + + if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) { + Configuration::instance()->enableInMemory(); + } + } + + /** + * @param class-string $className + */ + private function bootFoundryForDataProvider(string $className): void + { + if (!\is_subclass_of($className, TestCase::class)) { + return; + } + + // unit test + if (!\is_subclass_of($className, KernelTestCase::class)) { + Configuration::bootForDataProvider(UnitTestConfig::build()); + + return; + } + + // integration test + Configuration::bootForDataProvider(static function() use ($className): Configuration { + if (!KernelTestCaseHelper::getContainer($className)->has('.zenstruck_foundry.configuration')) { + throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); + } + + return KernelTestCaseHelper::getContainer($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type + }); + } +} diff --git a/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php new file mode 100644 index 000000000..774448b16 --- /dev/null +++ b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit\DataProvider; + +use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry; +use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber +{ + public function notify(Event\Test\DataProviderMethodFinished $event): void + { + PersistentObjectFromDataProviderRegistry::instance()->storeDatasetIfFoundryWasUsedInDataProvider( + $event->testMethod()->className(), + $event->testMethod()->methodName(), + ...$event->calledMethods(), + ); + + KernelTestCaseHelper::tearDownClass($event->testMethod()->className()); + + Configuration::shutdown(); + } +} diff --git a/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php new file mode 100644 index 000000000..81e675725 --- /dev/null +++ b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit\DataProvider; + +use PHPUnit\Event; +use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class TriggerDataProviderPersistenceOnTestPrepared implements Event\Test\PreparedSubscriber +{ + public function notify(Event\Test\Prepared $event): void + { + $test = $event->test(); + + if (!$test->isTestMethod()) { + return; + } + /** @var Event\Code\TestMethod $test */ + if (!$test->testData()->hasDataFromDataProvider() || $test->metadata()->isDataProvider()->isEmpty()) { + return; + } + + PersistentObjectFromDataProviderRegistry::instance()->triggerPersistenceForDataset( + $test->className(), + $test->methodName(), + $test->testData()->dataFromDataProvider()->dataSetName(), + ); + } +} diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index 240e8c4be..d813615f6 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -17,35 +17,79 @@ use PHPUnit\Runner; use PHPUnit\TextUI; use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\DataProvider\BootFoundryOnDataProviderMethodCalled; +use Zenstruck\Foundry\PHPUnit\DataProvider\ShutdownFoundryOnDataProviderMethodFinished; +use Zenstruck\Foundry\PHPUnit\DataProvider\TriggerDataProviderPersistenceOnTestPrepared; +use Zenstruck\Foundry\PHPUnit\ResetDatabase\ResetDatabaseBeforeEachTest; +use Zenstruck\Foundry\PHPUnit\ResetDatabase\ResetDatabaseBeforeFirstTest; /** * @internal * @author Nicolas PHILIPPE */ -final class FoundryExtension implements Runner\Extension\Extension -{ - public function bootstrap( - TextUI\Configuration\Configuration $configuration, - Runner\Extension\Facade $facade, - Runner\Extension\ParameterCollection $parameters, - ): void { - // shutdown Foundry if for some reason it has been booted before - if (Configuration::isBooted()) { - Configuration::shutdown(); +if (\interface_exists(Runner\Extension\Extension::class)) { + final class FoundryExtension implements Runner\Extension\Extension + { + private static bool $enabled = false; + + public function bootstrap( + TextUI\Configuration\Configuration $configuration, + Runner\Extension\Facade $facade, + Runner\Extension\ParameterCollection $parameters, + ): void { + // shutdown Foundry if for some reason it has been booted before + if (Configuration::isBooted()) { + Configuration::shutdown(); + } + + if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) { + // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used + $subscribers = [ + new BootFoundryOnDataProviderMethodCalled(), + new ShutdownFoundryOnDataProviderMethodFinished(), + + // must be added BEFORE ResetDatabaseBeforeEachTest + new TriggerDataProviderPersistenceOnTestPrepared(), + ]; + } + + $subscribers = [ + ...($subscribers ?? []), + new BuildStoryOnTestPrepared(), + new EnableInMemoryBeforeTest(), + new DisplayFakerSeedOnTestSuiteFinished(), + new BootFoundryOnPreparationStarted(), + new ShutdownFoundryOnTestFinished(), + new ResetDatabaseBeforeFirstTest(), + new ResetDatabaseBeforeEachTest(), + ]; + + $facade->registerSubscribers(...$subscribers); + + self::$enabled = true; } - $subscribers = [ - new BuildStoryOnTestPrepared(), - new EnableInMemoryBeforeTest(), - new DisplayFakerSeedOnTestSuiteFinished(), - ]; + public static function shouldBeEnabled(): bool + { + return !self::isEnabled() && ConstraintRequirement::from('>=10')->isSatisfiedBy(Runner\Version::id()); + } - if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) { - // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used - $subscribers[] = new BootFoundryOnDataProviderMethodCalled(); - $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished(); + public static function isEnabled(): bool + { + return self::$enabled; + } + } +} else { + final class FoundryExtension + { + public static function shouldBeEnabled(): bool // @phpstan-ignore return.tooWideBool + { + return false; } - $facade->registerSubscribers(...$subscribers); + public static function isEnabled(): bool // @phpstan-ignore return.tooWideBool + { + return false; + } } } diff --git a/src/PHPUnit/KernelTestCaseHelper.php b/src/PHPUnit/KernelTestCaseHelper.php new file mode 100644 index 000000000..0b1e9615a --- /dev/null +++ b/src/PHPUnit/KernelTestCaseHelper.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * @internal + */ +final class KernelTestCaseHelper +{ + /** + * @param class-string $class + */ + public static function getContainer(string $class): Container + { + if (!\is_subclass_of($class, KernelTestCase::class)) { + throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class)); + } + + return (\Closure::bind( + fn() => $class::getContainer(), + newThis: null, + newScope: $class, + ))(); + } + + /** + * @param class-string $class + */ + public static function tearDownClass(string $class): void + { + if (!\is_subclass_of($class, TestCase::class)) { + throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, TestCase::class)); + } + + (\Closure::bind( + fn() => $class::tearDownAfterClass(), + newThis: null, + newScope: $class, + ))(); + } + + /** + * @param class-string $class + */ + public static function bootKernel(string $class): KernelInterface + { + if (!\is_subclass_of($class, KernelTestCase::class)) { + throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class)); + } + + return (\Closure::bind( + fn() => $class::bootKernel(), + newThis: null, + newScope: $class, + ))(); + } + + /** + * @param class-string $class + */ + public static function ensureKernelShutdown(string $class): void + { + if (!\is_subclass_of($class, KernelTestCase::class)) { + throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class)); + } + + (\Closure::bind( + fn() => $class::ensureKernelShutdown(), + newThis: null, + newScope: $class, + ))(); + } +} diff --git a/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php new file mode 100644 index 000000000..f088004bd --- /dev/null +++ b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit\ResetDatabase; + +use PHPUnit\Event; +use PHPUnit\Event\Test\Prepared; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; +use Zenstruck\Foundry\PHPUnit\AttributeReader; +use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseBeforeEachTest implements Event\Test\PreparedSubscriber +{ + public function notify(Prepared $event): void + { + $test = $event->test(); + + if (!$test->isTestMethod()) { + return; + } + /** @var Event\Code\TestMethod $test */ + $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents( + ResetDatabase::class, + new \ReflectionClass($test->className()) + ); + + if ([] === $resetDatabaseAttributes) { + return; + } + + ResetDatabaseManager::resetBeforeEachTest( + static fn() => KernelTestCaseHelper::bootKernel($test->className()), + static fn() => KernelTestCaseHelper::ensureKernelShutdown($test->className()), + ); + } +} diff --git a/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php new file mode 100644 index 000000000..a70654ffe --- /dev/null +++ b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit\ResetDatabase; + +use PHPUnit\Event; +use PHPUnit\Event\TestSuite\Started as TestSuiteStarted; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; +use Zenstruck\Foundry\PHPUnit\AttributeReader; +use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseBeforeFirstTest implements Event\TestSuite\StartedSubscriber +{ + public function notify(TestSuiteStarted $event): void + { + if (!$event->testSuite()->isForTestClass()) { + return; + } + + $testClassName = $event->testSuite()->name(); + + if (!\class_exists($testClassName)) { + return; + } + + $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents( + ResetDatabase::class, + new \ReflectionClass($testClassName) + ); + + if ([] === $resetDatabaseAttributes) { + return; + } + + ResetDatabaseManager::resetBeforeFirstTest( + static fn() => KernelTestCaseHelper::bootKernel($testClassName), + static fn() => KernelTestCaseHelper::ensureKernelShutdown($testClassName), + ); + } +} diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnTestFinished.php similarity index 52% rename from src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php rename to src/PHPUnit/ShutdownFoundryOnTestFinished.php index b028394b3..bfa9b99fb 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/ShutdownFoundryOnTestFinished.php @@ -14,17 +14,16 @@ namespace Zenstruck\Foundry\PHPUnit; use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; /** * @internal * @author Nicolas PHILIPPE */ -final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber +final class ShutdownFoundryOnTestFinished implements Event\Test\FinishedSubscriber { - public function notify(Event\Test\DataProviderMethodFinished $event): void + public function notify(Event\Test\Finished $event): void { - if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { - $event->testMethod()->className()::_shutdownAfterDataProvider(); - } + Configuration::shutdown(); } } diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index be6351864..5d06eebef 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -16,6 +16,7 @@ use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; use Zenstruck\Foundry\Object\Hydrator; +use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy; use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist; /** @@ -152,7 +153,7 @@ private function _autoRefresh(): void // we don't want that "transparent" calls to _refresh() to trigger a PersistenceNotAvailable exception // or a RefreshObjectFailed exception when the object was deleted $this->_refresh(); - } catch (PersistenceNotAvailable|ObjectNoLongerExist) { + } catch (PersistenceNotAvailable|ObjectNoLongerExist|NoPersistenceStrategy) { } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 8a7b8d6c2..ea5ca2a39 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -241,7 +241,7 @@ public function create(callable|array $attributes = []): object && $this->isPersisting() && !$this instanceof PersistentProxyObjectFactory ) { - return ProxyGenerator::wrapFactoryNativeProxy($this, $attributes); + return PersistentObjectFromDataProviderRegistry::instance()->deferObjectCreation($this->with($attributes)); } $object = parent::create($attributes); diff --git a/src/Persistence/PersistentObjectFromDataProviderRegistry.php b/src/Persistence/PersistentObjectFromDataProviderRegistry.php new file mode 100644 index 000000000..47859c72a --- /dev/null +++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use PHPUnit\Event\Code\ClassMethod; + +/** + * If a persistent object has been created in a data provider, we need to initialize the lazy object, + * which will trigger the object to be persisted. + * + * Otherwise, such a test would not pass: + * ```php + * #[DataProvider('provide')] + * public function testSomething(MyEntity $entity): void + * { + * MyEntityFactory::assert()->count(1); + * } + * + * public static function provide(): iterable + * { + * yield [MyEntityFactory::createOne()]; + * } + * ``` + * + * Sadly, this cannot be done directly a subscriber, since PHPUnit does not give access to the actual tests instances. + * + * ⚠️ This class is highly hacky! + * + * If we detect that a persisting object was created in a data provider, we collect the "datasets" of the test, + * and we trigger the persistence of these objects before the test is executed. + * + * This means that the data providers using Foundry are called twice. + * To prevent the persisted object from being different from the one returned by the data provider, we use a "buffer" so + * that we can return the same object for each data provider call. + * + * @internal + */ +final class PersistentObjectFromDataProviderRegistry +{ + private static ?self $instance = null; + + /** @var array> */ + private array $datasets = []; + + /** @var list */ + private array $objectsBuffer = []; + + private bool $shouldReturnObjectFromBuffer = false; + + public static function instance(): self + { + return self::$instance ?? self::$instance = new self(); + } + + public function storeDatasetIfFoundryWasUsedInDataProvider(string $className, string $methodName, ClassMethod ...$calledMethods): void + { + if (count($this->objectsBuffer) === 0) { + return; + } + + $this->shouldReturnObjectFromBuffer = true; + + $testCaseContext = $this->testCaseContext($className, $methodName); + $this->datasets[$testCaseContext] = []; + + foreach ($calledMethods as $calledMethod) { + $dataProviderResult = "{$calledMethod->className()}::{$calledMethod->methodName()}"(); // @phpstan-ignore callable.nonCallable + + if (!\is_array($dataProviderResult)) { + $dataProviderResult = \iterator_to_array($dataProviderResult); + } + + $this->datasets[$testCaseContext] = [...$this->datasets[$testCaseContext], ...$dataProviderResult]; + } + + $this->shouldReturnObjectFromBuffer = false; + + if (count($this->objectsBuffer) !== 0) { // @phpstan-ignore notIdentical.alwaysTrue + throw new \InvalidArgumentException("No object found. Hint: make sure you're not creating a randomized number of objects with Foundry in a data provider, as they are not supported."); + } + } + + /** + * @template T of object + * + * @param PersistentObjectFactory $factory + * + * @return ($factory is PersistentProxyObjectFactory ? T&Proxy : T) + */ + public function deferObjectCreation(PersistentObjectFactory $factory): object + { + if (!$factory->isPersisting()) { + return $factory->create(); + } + + if (!$this->shouldReturnObjectFromBuffer) { + return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory); + } + + if (count($this->objectsBuffer) === 0) { + throw new \InvalidArgumentException("No object found. Hint: make sure you're not creating a randomized number of objects with Foundry in a data provider, as they are not supported."); + } + + return \array_shift($this->objectsBuffer); // @phpstan-ignore return.type + } + + public function triggerPersistenceForDataset(string $className, string $methodName, int|string $dataSetName): void + { + $testCaseContext = $this->testCaseContext($className, $methodName); + + if (!isset($this->datasets[$testCaseContext][$dataSetName])) { + return; + } + + initialize_lazy_object($this->datasets[$testCaseContext][$dataSetName]); + + unset($this->datasets[$testCaseContext][$dataSetName]); + } + + private function testCaseContext(string $className, string $methodName): string + { + return "{$className}::{$methodName}"; + } +} diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index 583607cde..a7ed99283 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -42,7 +42,7 @@ final public function create(callable|array $attributes = []): object { $configuration = Configuration::instance(); if ($configuration->inADataProvider() && $this->isPersisting()) { - return ProxyGenerator::wrapFactory($this, $attributes); + return PersistentObjectFromDataProviderRegistry::instance()->deferObjectCreation($this->with($attributes)); } return proxy(parent::create($attributes)); // @phpstan-ignore function.unresolvableReturnType diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index 27aeab986..21df22311 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -47,47 +47,37 @@ public static function wrap(object $object): Proxy return self::generateClassFor($object)::createLazyProxy(static fn() => $object); // @phpstan-ignore staticMethod.unresolvableReturnType } - /** - * @template T of object - * - * @param PersistentProxyObjectFactory $factory - * @phpstan-param Attributes $attributes - * - * @return T&Proxy - */ - public static function wrapFactory(PersistentProxyObjectFactory $factory, callable|array $attributes): Proxy - { - return self::generateClassFor($factory)::createLazyProxy(static function() use ($factory, $attributes) { // @phpstan-ignore staticMethod.notFound - if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { - throw new \LogicException('Cannot access to a persisted object from a data provider.'); - } - - return self::unwrap($factory->create($attributes)); - }); - } - /** * @template T of object * * @param PersistentObjectFactory $factory - * @phpstan-param Attributes $attributes * - * @return T + * @return ($factory is PersistentProxyObjectFactory ? T&Proxy : T) */ - public static function wrapFactoryNativeProxy(PersistentObjectFactory $factory, callable|array $attributes): object + public static function wrapFactory(PersistentObjectFactory $factory): object { + if ($factory instanceof PersistentProxyObjectFactory) { + return self::generateClassFor($factory)::createLazyProxy(static function() use ($factory) { // @phpstan-ignore staticMethod.notFound + if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { + throw new \LogicException('Cannot access to a persisted object from a data provider.'); + } + + return ProxyGenerator::unwrap($factory->create()); + }); + } + if (\PHP_VERSION_ID < 80400) { throw new \LogicException('Native proxy generation requires PHP 8.4 or higher.'); } $reflector = new \ReflectionClass($factory::class()); - return $reflector->newLazyProxy(static function() use ($factory, $attributes) { + return $reflector->newLazyProxy(static function() use ($factory) { if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { throw new \LogicException('Cannot access to a persisted object from a data provider.'); } - return $factory->create($attributes); + return $factory->create(); }); } diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index 50d0768a6..117e6fa95 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -230,7 +230,7 @@ function assert_not_persisted(object $object, string $message = '{entity} is per /** * @internal */ -function initialize_proxy_object(mixed $what): void +function initialize_lazy_object(mixed $what): void { if ( \PHP_VERSION_ID >= 80400 @@ -244,7 +244,7 @@ function initialize_proxy_object(mixed $what): void match (true) { $what instanceof Proxy => $what->_initializeLazyObject(), - \is_array($what) => \array_map(initialize_proxy_object(...), $what), + \is_array($what) => \array_map(initialize_lazy_object(...), $what), default => true, // do nothing }; } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index 8bd39cf89..6b5f80291 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -15,8 +15,9 @@ use PHPUnit\Framework\Attributes\Before; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use function Zenstruck\Foundry\Persistence\initialize_proxy_object; +use function Zenstruck\Foundry\Persistence\initialize_lazy_object; /** * @author Kevin Bond @@ -27,9 +28,15 @@ trait Factories * @internal * @before */ - #[Before] + #[Before(5)] public function _beforeHook(): void { + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.9', \sprintf('Trait %s is deprecated and will be removed in Foundry 3. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.9.md to upgrade.', Factories::class)); + + return; + } + $this->_bootFoundry(); $this->_loadDataProvidedProxies(); } @@ -38,47 +45,13 @@ public function _beforeHook(): void * @internal * @after */ - #[After] + #[After(5)] public static function _shutdownFoundry(): void { - Configuration::shutdown(); - } - - /** - * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled - * @internal - */ - public static function _bootForDataProvider(): void - { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType - // unit test - Configuration::bootForDataProvider(UnitTestConfig::build()); - + if (FoundryExtension::isEnabled()) { return; } - // integration test - Configuration::bootForDataProvider(static function(): Configuration { - if (!static::getContainer()->has('.zenstruck_foundry.configuration')) { // @phpstan-ignore staticMethod.notFound - throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); - } - - return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound, return.type - }); - } - - /** - * @internal - * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished - */ - public static function _shutdownAfterDataProvider(): void - { - if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType - self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound - static::$class = null; // @phpstan-ignore staticProperty.notFound - static::$kernel = null; // @phpstan-ignore staticProperty.notFound - static::$booted = false; // @phpstan-ignore staticProperty.notFound - } Configuration::shutdown(); } @@ -132,8 +105,10 @@ private function _loadDataProvidedProxies(): void return; } - $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound, method.internal + $providedData = \method_exists($this, 'getProvidedData') // @phpstan-ignore function.impossibleType + ? $this->getProvidedData() // @phpstan-ignore method.notFound + : $this->providedData(); // @phpstan-ignore method.internal - initialize_proxy_object($providedData); + initialize_lazy_object($providedData); } } diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index d17e8bf39..d9dfb8fb4 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -14,7 +14,10 @@ use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\ResetDatabase as ResetDatabaseAttribute; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; +use Zenstruck\Foundry\PHPUnit\AttributeReader; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond @@ -28,6 +31,14 @@ trait ResetDatabase #[BeforeClass] public static function _resetDatabaseBeforeFirstTest(): void { + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.9', \sprintf('Trait "%s" is deprecated and will be removed in Foundry 3. Use attribute "%s" instead. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.9.md to upgrade.', ResetDatabase::class, ResetDatabaseAttribute::class)); + + if (self::_classHasResetDatabaseAttribute()) { + return; + } + } + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } @@ -42,9 +53,13 @@ public static function _resetDatabaseBeforeFirstTest(): void * @internal * @before */ - #[Before] + #[Before(10)] public static function _resetDatabaseBeforeEachTest(): void { + if (FoundryExtension::isEnabled() && self::_classHasResetDatabaseAttribute()) { + return; + } + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } @@ -54,4 +69,17 @@ public static function _resetDatabaseBeforeEachTest(): void static fn() => static::ensureKernelShutdown(), ); } + + /** + * @internal + */ + private static function _classHasResetDatabaseAttribute(): bool + { + $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents( + ResetDatabaseAttribute::class, + new \ReflectionClass(static::class) + ); + + return [] !== $resetDatabaseAttributes; + } } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 447303625..4963044f6 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -33,6 +33,7 @@ use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; use Zenstruck\Foundry\ORM\ResetDatabase\SchemaDatabaseResetter; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond @@ -41,7 +42,7 @@ final class ZenstruckFoundryBundle extends AbstractBundle implements CompilerPas { public function boot(): void { - if ($this->container) { + if ($this->container && (!Configuration::isBooted() || !FoundryExtension::isEnabled())) { Configuration::boot($this->container->get('.zenstruck_foundry.configuration')); // @phpstan-ignore argument.type } } diff --git a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php index 2cd3d777b..f00eb4453 100644 --- a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -38,6 +38,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load parent::configureContainer($c, $loader); $c->loadFromExtension('zenstruck_foundry', [ + 'persistence' => ['flush_once' => true], + 'enable_auto_refresh_with_lazy_objects' => self::usePHP84LazyObjects(), 'global_state' => [ GlobalStory::class, GlobalInvokableService::class, diff --git a/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php index 0a3369277..553fef5e0 100644 --- a/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php +++ b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php @@ -15,7 +15,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Stories\EntityStory; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -26,5 +25,5 @@ #[WithStory(EntityStory::class)] abstract class ParentClassWithStoryAttributeTestCase extends KernelTestCase { - use Factories, RequiresORM, ResetDatabase; + use RequiresORM, ResetDatabase; } diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php index f88ab6c8e..90ce7e001 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -19,7 +19,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; @@ -35,7 +34,7 @@ #[WithStory(EntityStory::class)] final class WithStoryOnClassTest extends KernelTestCase { - use Factories, RequiresORM, ResetDatabase; + use RequiresORM, ResetDatabase; #[Test] public function can_use_story_in_attribute(): void diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php index f1df60672..8e7bd6aed 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -19,7 +19,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; @@ -35,7 +34,7 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class WithStoryOnMethodTest extends KernelTestCase { - use Factories, RequiresORM, ResetDatabase; + use RequiresORM, ResetDatabase; #[Test] #[WithStory(EntityStory::class)] diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php index 579e34db3..cdb344787 100644 --- a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -20,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Object1; @@ -34,8 +33,6 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTestCase { - use Factories; - #[Test] #[DataProvider('createObjectFromServiceFactoryInDataProvider')] public function it_can_create_one_object_in_data_provider(?Object1 $providedData, string $expected): void diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php index db2ec5a72..aad8d9640 100644 --- a/tests/Integration/DataProvider/DataProviderInUnitTest.php +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -21,7 +21,6 @@ use PHPUnit\Framework\TestCase; use Zenstruck\Foundry\Persistence\ProxyGenerator; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; @@ -40,8 +39,6 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderInUnitTest extends TestCase { - use Factories; - #[Test] #[DataProvider('createObjectWithObjectFactoryInDataProvider')] public function assert_it_can_create_object_with_object_factory_in_data_provider(mixed $providedData, mixed $expectedData): void diff --git a/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php index 824a6e5f9..d570d0163 100644 --- a/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php +++ b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php @@ -23,7 +23,6 @@ use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Persistence\ProxyGenerator; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; @@ -39,7 +38,6 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderWithInMemoryTest extends KernelTestCase { - use Factories; use RequiresORM; // needed to use the entity manager use ResetDatabase; diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentDocumentFactoryTest.php b/tests/Integration/DataProvider/DataProviderWithPersistentDocumentFactoryTest.php new file mode 100644 index 000000000..e8d0a4032 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithPersistentDocumentFactoryTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresEnvironmentVariable; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; +use Zenstruck\Foundry\Tests\Integration\RequiresMongo; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=12 + */ +#[RequiresPhpunit('>=12')] +#[RequiresPhp('>=8.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[RequiresEnvironmentVariable('USE_PHP_84_LAZY_OBJECTS', '1')] +final class DataProviderWithPersistentDocumentFactoryTest extends DataProviderWithPersistentFactoryTestCase +{ + use RequiresMongo; + + protected static function factory(): PersistentObjectFactory + { + return GenericDocumentFactory::new(); + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php b/tests/Integration/DataProvider/DataProviderWithPersistentEntityFactoryTest.php similarity index 54% rename from tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php rename to tests/Integration/DataProvider/DataProviderWithPersistentEntityFactoryTest.php index e0c918556..74a732e77 100644 --- a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php +++ b/tests/Integration/DataProvider/DataProviderWithPersistentEntityFactoryTest.php @@ -20,11 +20,12 @@ use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; /** * @author Nicolas PHILIPPE @@ -34,10 +35,9 @@ #[RequiresPhp('>=8.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[RequiresEnvironmentVariable('USE_PHP_84_LAZY_OBJECTS', '1')] -final class DataProviderWithPersistentFactoryAndPHP84InKernelTest extends KernelTestCase +final class DataProviderWithPersistentEntityFactoryTest extends DataProviderWithPersistentFactoryTestCase { - use Factories; - use ResetDatabase; + use RequiresORM; #[Test] #[DataProvider('createOneObjectInDataProvider')] @@ -47,7 +47,6 @@ public function assert_it_can_create_one_object_in_data_provider(?GenericModel $ self::assertNotNull($providedData); self::assertFalse((new \ReflectionClass($providedData))->isUninitializedLazyObject($providedData)); - self::assertSame('value set in data provider', $providedData->getProp1()); } public static function createOneObjectInDataProvider(): iterable @@ -55,41 +54,10 @@ public static function createOneObjectInDataProvider(): iterable yield 'createOne()' => [ GenericEntityFactory::createOne(['prop1' => 'value set in data provider']), ]; - - yield 'create()' => [ - GenericEntityFactory::new()->create(['prop1' => 'value set in data provider']), - ]; } - #[Test] - #[DataProvider('createMultipleObjectsInDataProvider')] - public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + protected static function factory(): PersistentObjectFactory { - self::assertIsArray($providedData); - GenericEntityFactory::assert()->count(2); - - foreach ($providedData as $providedDatum) { - self::assertFalse((new \ReflectionClass($providedDatum))->isUninitializedLazyObject($providedDatum)); - } - - self::assertSame('prop 1', $providedData[0]->getProp1()); - self::assertSame('prop 2', $providedData[1]->getProp1()); - } - - public static function createMultipleObjectsInDataProvider(): iterable - { - yield 'createSequence()' => [ - GenericEntityFactory::createSequence([ - ['prop1' => 'prop 1'], - ['prop1' => 'prop 2'], - ]), - ]; - - yield 'FactoryCollection::create()' => [ - GenericEntityFactory::new()->sequence([ - ['prop1' => 'prop 1'], - ['prop1' => 'prop 2'], - ])->create(), - ]; + return GenericEntityFactory::new(); } } diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php deleted file mode 100644 index 1a6f07194..000000000 --- a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php +++ /dev/null @@ -1,189 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\DataProvider; - -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\IgnoreDeprecations; -use PHPUnit\Framework\Attributes\RequiresPhp; -use PHPUnit\Framework\Attributes\RequiresPhpunit; -use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; -use PHPUnit\Framework\Attributes\Test; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Persistence\Proxy; -use Zenstruck\Foundry\Persistence\ProxyGenerator; -use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; -use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; - -/** - * @author Nicolas PHILIPPE - * @requires PHPUnit >=11.4 - */ -#[RequiresPhpunit('>=11.4')] -#[RequiresPhpunitExtension(FoundryExtension::class)] -#[IgnoreDeprecations] -abstract class DataProviderWithPersistentFactoryInKernelTestCase extends KernelTestCase -{ - use Factories; - use ResetDatabase; - - #[Test] - #[DataProvider('createOneProxyObjectInDataProvider')] - public function assert_it_can_create_one_object_in_data_provider(?GenericModel $providedData): void - { - static::proxyFactory()::assert()->count(1); - - self::assertInstanceOf(Proxy::class, $providedData); - self::assertNotInstanceOf(Proxy::class, ProxyGenerator::unwrap($providedData)); // asserts two proxies are not nested - self::assertSame('value set in data provider', $providedData->getProp1()); - } - - public static function createOneProxyObjectInDataProvider(): iterable - { - yield 'createOne()' => [ - static::proxyFactory()::createOne(['prop1' => 'value set in data provider']), - ]; - - yield 'create()' => [ - static::proxyFactory()->create(['prop1' => 'value set in data provider']), - ]; - } - - #[Test] - #[DataProvider('createMultipleObjectsInDataProvider')] - public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void - { - self::assertIsArray($providedData); - static::proxyFactory()::assert()->count(2); - self::assertSame('prop 1', $providedData[0]->getProp1()); - self::assertSame('prop 2', $providedData[1]->getProp1()); - } - - public static function createMultipleObjectsInDataProvider(): iterable - { - yield 'createSequence()' => [ - static::proxyFactory()::createSequence([ - ['prop1' => 'prop 1'], - ['prop1' => 'prop 2'], - ]), - ]; - - yield 'FactoryCollection::create()' => [ - static::proxyFactory()->sequence([ - ['prop1' => 'prop 1'], - ['prop1' => 'prop 2'], - ])->create(), - ]; - } - - #[Test] - #[DataProvider('useGetterOnProxyObjectCreatedInDataProvider')] - public function assert_using_getter_proxy_object_created_in_a_data_provider_throws(?\Throwable $e): void - { - self::assertInstanceOf(\LogicException::class, $e); - self::assertStringStartsWith('Cannot access to a persisted object from a data provider.', $e->getMessage()); - } - - public static function useGetterOnProxyObjectCreatedInDataProvider(): iterable - { - try { - static::proxyFactory()::createOne()->getProp1(); - } catch (\Throwable $e) { - } - - yield [$e ?? null]; - } - - #[Test] - #[DataProvider('throwsExceptionWhenCreatingObjectInDataProvider')] - #[RequiresPhp('<8.4')] - public function it_throws_when_creating_persisted_object_with_non_proxy_factory_in_data_provider_without_php_84(?\Throwable $e): void - { - self::assertInstanceOf(\LogicException::class, $e); - self::assertStringStartsWith( - 'Cannot create object in a data provider for non-proxy factories.', - $e->getMessage() - ); - } - - public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterable - { - try { - static::factory()::createOne(); - } catch (\Throwable $e) { - } - - yield [$e ?? null]; - } - - #[Test] - #[DataProvider('createOneObjectInDataProvider')] - #[RequiresPhp('>=8.4')] - public function assert_it_can_create_one_object_in_data_provider_without_proxy_with_php_84(mixed $providedData): void - { - static::proxyFactory()::assert()->count(1); - - self::assertInstanceOf(GenericModel::class, $providedData); - self::assertSame('value set in data provider', $providedData->getProp1()); - } - - public static function createOneObjectInDataProvider(): iterable - { - yield 'createOne()' => [ - static::proxyFactory()::createOne(['prop1' => 'value set in data provider']), - ]; - - yield 'create()' => [ - static::proxyFactory()->create(['prop1' => 'value set in data provider']), - ]; - } - - #[Test] - #[DataProvider('useGetterOnObjectCreatedInDataProvider')] - public function assert_it_can_use_getter_on_non_persisted_object_created_in_data_provider( - string $providedData, - mixed $expectedData, - ): void { - self::assertEquals($expectedData, ProxyGenerator::unwrap($providedData)); - } - - public static function useGetterOnObjectCreatedInDataProvider(): iterable - { - yield 'object factory' => [Object1Factory::createOne()->getProp1(), 'router-constructor']; - yield 'persistent factory' => [static::factory()::new()->withoutPersisting()->create()->getProp1(), 'default1']; - yield 'persistent factory using many' => [ - static::factory()::new()->withoutPersisting()->many(1)->create()[0]->getProp1(), - 'default1', - ]; - yield 'proxy factory' => [static::proxyFactory()::new()->withoutPersisting()->create()->_real(withAutoRefresh: false)->getProp1(), 'default1']; - yield 'proxy factory using many' => [ - static::proxyFactory()::new()->withoutPersisting()->many(1)->create()[0]->_real(withAutoRefresh: false)->getProp1(), - 'default1', - ]; - } - - /** - * @return PersistentProxyObjectFactory - */ - abstract protected static function proxyFactory(): PersistentProxyObjectFactory; - - /** - * @return PersistentObjectFactory - */ - abstract protected static function factory(): PersistentObjectFactory; -} diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryTestCase.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryTestCase.php new file mode 100644 index 000000000..1b1960ebf --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryTestCase.php @@ -0,0 +1,199 @@ +count(1); + + self::assertNotNull($providedData); + self::assertSame('value set in data provider', $providedData->getProp1()); + + assert_persisted($providedData); + } + + public static function createOneObjectInDataProvider(): iterable + { + yield 'createOne()' => [ + static::factory()::createOne(['prop1' => 'value set in data provider']), + ]; + + yield 'create()' => [ + static::factory()->create(['prop1' => 'value set in data provider']), + ]; + } + + #[Test] + #[DataProvider('createMultipleObjectsInDataProvider')] + public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + { + self::assertIsArray($providedData); + static::factory()::assert()->count(2); + + self::assertSame('prop 1', $providedData[0]->getProp1()); + self::assertSame('prop 2', $providedData[1]->getProp1()); + } + + public static function createMultipleObjectsInDataProvider(): iterable + { + yield 'createSequence()' => [ + static::factory()::createSequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ]), + ]; + + yield 'sequence()->create()' => [ + static::factory()->sequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ])->create(), + ]; + + yield 'createMany()' => [ + static::factory()::createMany(2, static fn(int $i) => ['prop1' => "prop $i"]), + ]; + + yield 'many()->create()' => [ + static::factory()->many(2)->create(static fn(int $i) => ['prop1' => "prop $i"]), + ]; + } + + #[Test] + #[DataProvider('dataProviderRetuningArray')] + public function assert_it_can_use_data_provider_returning_array(?GenericModel $providedData): void + { + static::factory()::assert()->count(1); + + self::assertNotNull($providedData); + self::assertSame('value set in data provider', $providedData->getProp1()); + } + + public static function dataProviderRetuningArray(): array + { + return [ + [ + static::factory()::createOne(['prop1' => 'value set in data provider']), + ], + [ + static::factory()->create(['prop1' => 'value set in data provider']), + ] + ]; + } + + #[Test] + #[DataProvider('multipleDataProviders1')] + #[DataProvider('multipleDataProviders2')] + public function assert_it_can_use_multiple_data_providers(array $providedData, int $expectedCount): void + { + static::factory()::assert()->count($expectedCount); + + foreach ($providedData as $providedDatum) { + self::assertNotNull($providedDatum); + self::assertSame('value set in data provider', $providedDatum->getProp1()); + } + } + + public static function multipleDataProviders1(): iterable + { + yield [ + static::factory()::createMany(1, ['prop1' => 'value set in data provider']), + 1 + ]; + } + + public static function multipleDataProviders2(): iterable + { + yield [ + static::factory()::createMany(2, ['prop1' => 'value set in data provider']), + 2 + ]; + } + + #[Test] + #[DataProvider('dataProviderUsingRandomRanges')] + public function assert_data_provider_throws_when_using_range_in_data_provider(?\Throwable $throwable): void + { + self::assertInstanceOf(\InvalidArgumentException::class, $throwable); + self::assertSame('Using randomized "range" factory in data provider is not supported.', $throwable->getMessage()); + } + + public static function dataProviderUsingRandomRanges(): iterable + { + try { + GenericEntityFactory::createRange(1, 2); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + + try { + GenericEntityFactory::new()->range(1, 2); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } + + #[Test] + #[DataProvider('useGetterOnProxyObjectCreatedInDataProvider')] + public function assert_using_getter_proxy_object_created_in_a_data_provider_throws(?\Throwable $e): void + { + self::assertInstanceOf(\LogicException::class, $e); + self::assertStringStartsWith('Cannot access to a persisted object from a data provider.', $e->getMessage()); + } + + public static function useGetterOnProxyObjectCreatedInDataProvider(): iterable + { + try { + static::factory()::createOne()->getProp1(); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } + + #[Test] + #[DataProvider('useGetterOnObjectCreatedInDataProvider')] + public function assert_it_can_use_getter_on_non_persisted_object_created_in_data_provider( + string $providedData, + mixed $expectedData, + ): void { + self::assertEquals($expectedData, ProxyGenerator::unwrap($providedData)); + } + + public static function useGetterOnObjectCreatedInDataProvider(): iterable + { + yield 'object factory' => [Object1Factory::createOne()->getProp1(), 'router-constructor']; + yield 'persistent factory' => [static::factory()->withoutPersisting()->create()->getProp1(), 'default1']; + yield 'persistent factory using many' => [ + static::factory()->withoutPersisting()->many(1)->create()[0]->getProp1(), + 'default1', + ]; + } + + /** + * @return PersistentObjectFactory + */ + abstract protected static function factory(): PersistentObjectFactory; +} diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/DataProviderWithProxyPersistentDocumentFactoryTest.php similarity index 78% rename from tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php rename to tests/Integration/DataProvider/DataProviderWithProxyPersistentDocumentFactoryTest.php index f18c5136b..138d66d0a 100644 --- a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/DataProvider/DataProviderWithProxyPersistentDocumentFactoryTest.php @@ -27,17 +27,12 @@ #[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[IgnoreDeprecations] -final class GenericDocumentProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase +final class DataProviderWithProxyPersistentDocumentFactoryTest extends DataProviderWithPersistentFactoryTestCase { use RequiresMongo; - protected static function proxyFactory(): GenericProxyDocumentFactory + protected static function factory(): GenericProxyDocumentFactory { return GenericProxyDocumentFactory::new(); } - - protected static function factory(): PersistentObjectFactory - { - return GenericDocumentFactory::new(); - } } diff --git a/tests/Integration/DataProvider/DataProviderWithProxyPersistentEntityFactoryTest.php b/tests/Integration/DataProvider/DataProviderWithProxyPersistentEntityFactoryTest.php new file mode 100644 index 000000000..68d91b7ad --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithProxyPersistentEntityFactoryTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyGenerator; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[IgnoreDeprecations] +final class DataProviderWithProxyPersistentEntityFactoryTest extends DataProviderWithPersistentFactoryTestCase +{ + use RequiresORM; + + #[Test] + #[DataProvider('createOneProxyObjectInDataProvider')] + public function assert_provided_data_is_proxy(?GenericModel $providedData): void + { + static::factory()::assert()->count(1); + + self::assertInstanceOf(Proxy::class, $providedData); + self::assertNotInstanceOf(Proxy::class, ProxyGenerator::unwrap($providedData)); // asserts two proxies are not nested + + static::factory()::assert()->count(1); + $providedData->_assertPersisted(); + } + + public static function createOneProxyObjectInDataProvider(): iterable + { + yield [ + static::factory()::createOne(), + ]; + } + + #[Test] + #[DataProvider('throwsExceptionWhenCreatingObjectInDataProvider')] + #[RequiresPhp('<8.4')] + public function it_throws_when_creating_persisted_object_with_non_proxy_factory_in_data_provider_without_php_84(?\Throwable $e): void + { + self::assertInstanceOf(\LogicException::class, $e); + self::assertStringStartsWith( + 'Cannot create object in a data provider for non-proxy factories.', + $e->getMessage() + ); + } + + public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterable + { + try { + GenericEntityFactory::createOne(); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } + + protected static function factory(): GenericProxyEntityFactory + { + return GenericProxyEntityFactory::new(); + } +} diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php deleted file mode 100644 index 1f5eadb08..000000000 --- a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\DataProvider; - -use PHPUnit\Framework\Attributes\IgnoreDeprecations; -use PHPUnit\Framework\Attributes\RequiresPhpunit; -use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; -use Zenstruck\Foundry\Tests\Integration\RequiresORM; - -/** - * @author Nicolas PHILIPPE - * @requires PHPUnit >=11.4 - */ -#[RequiresPhpunit('>=11.4')] -#[RequiresPhpunitExtension(FoundryExtension::class)] -#[IgnoreDeprecations] -final class GenericEntityProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase -{ - use RequiresORM; - - protected static function proxyFactory(): GenericProxyEntityFactory - { - return GenericProxyEntityFactory::new(); - } - - protected static function factory(): PersistentObjectFactory - { - return GenericEntityFactory::new(); - } -} diff --git a/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php b/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php index c77c72559..1a7f3fec6 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php +++ b/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php @@ -18,6 +18,8 @@ abstract class AnotherBaseTestCase extends KernelTestCase { + use SkipWithPHPUnitExtension; + // @phpstan-ignore missingType.generics public function useProxyClass(Proxy $proxy): void { diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php index 23e72634a..a2461a5ff 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php @@ -23,7 +23,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithBothTraitsInWrongOrderTest extends KernelTestCase { - use Factories, ResetDatabase; + use Factories, ResetDatabase, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php index fd6de3f93..4d84f3e2d 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php @@ -23,7 +23,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithBothTraitsTest extends KernelTestCase { - use Factories, ResetDatabase; + use Factories, ResetDatabase, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php index 80d40751d..fdb7cdf4e 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php @@ -22,7 +22,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithFactoriesTraitTest extends KernelTestCase { - use Factories; + use Factories, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php similarity index 90% rename from tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php rename to tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php index 889784d62..6681111fd 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php @@ -18,8 +18,9 @@ use Zenstruck\Foundry\Test\ResetDatabase; #[RequiresPhpunit('>=11.0')] -final class KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait extends KernelTestCase +final class KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest extends KernelTestCase { use KernelTestCaseWithoutFactoriesTrait; use ResetDatabase; + use SkipWithPHPUnitExtension; } diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php similarity index 77% rename from tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php rename to tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php index 38669bd1c..ca9bf444d 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php @@ -17,7 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; #[RequiresPhpunit('>=11.0')] -final class KernelTestWithoutFactoriesTrait extends KernelTestCase +final class KernelTestWithoutFactoriesTest extends KernelTestCase { - use KernelTestCaseWithoutFactoriesTrait; + use KernelTestCaseWithoutFactoriesTrait, SkipWithPHPUnitExtension; } diff --git a/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php new file mode 100644 index 000000000..f489d99cf --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; + +/** + * @phpstan-require-extends TestCase + */ +trait SkipWithPHPUnitExtension +{ + #[Before] + public function _skipWithPHPUnitExtension(): void + { + if (FoundryExtension::isEnabled()) { + self::markTestSkipped('This test requires *NOT* using Foundry\'s PHUnit extension.'); + } + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php index b1d2b59f9..7b416194a 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php @@ -22,7 +22,7 @@ #[RequiresPhpunit('>=11.0')] final class UnitTestCaseWithFactoriesTraitTest extends TestCase { - use Factories; + use Factories, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php index 2275b1af3..9a8e1c043 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php @@ -22,6 +22,8 @@ #[RequiresPhpunit('>=11.0')] final class UnitTestCaseWithoutFactoriesTraitTest extends TestCase { + use SkipWithPHPUnitExtension; + #[Test] public function should_throw(): void { diff --git a/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..9b9c43cfd --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage\UsingExtension; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class KernelTestCaseWithoutFactoriesTraitTest extends KernelTestCase +{ + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function should_not_throw_even_when_kernel_is_booted(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..26fa7d1ca --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage\UsingExtension; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class UnitTestCaseWithoutFactoriesTraitTest extends TestCase +{ + #[Test] + public function should_not_throw(): void + { + $this->expectNotToPerformAssertions(); + + Object1Factory::createOne(); + } +} diff --git a/tests/Integration/InMemory/DoctrineInMemoryDecoratorTest.php b/tests/Integration/InMemory/DoctrineInMemoryDecoratorTest.php index c36258021..c5aeacbe0 100644 --- a/tests/Integration/InMemory/DoctrineInMemoryDecoratorTest.php +++ b/tests/Integration/InMemory/DoctrineInMemoryDecoratorTest.php @@ -20,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\InMemory\AsInMemoryTest; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixture\Entity\WithEmbeddableEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; @@ -41,8 +40,6 @@ #[AsInMemoryTest] final class DoctrineInMemoryDecoratorTest extends KernelTestCase { - use Factories; - /** * @test */ diff --git a/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php index 9444f67c7..b2cf535db 100644 --- a/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php +++ b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php @@ -20,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\InMemory\AsInMemoryTest; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Entity\Address; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; @@ -34,7 +33,6 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class InMemoryAttributeOnMethodTest extends KernelTestCase { - use Factories; use RequiresORM; use ResetDatabase; diff --git a/tests/Integration/InMemory/InMemoryTest.php b/tests/Integration/InMemory/InMemoryTest.php index 272c7df4d..a43f0f1ff 100644 --- a/tests/Integration/InMemory/InMemoryTest.php +++ b/tests/Integration/InMemory/InMemoryTest.php @@ -20,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\InMemory\AsInMemoryTest; use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Entity\Address; use Zenstruck\Foundry\Tests\Fixture\Entity\Category; @@ -41,7 +40,6 @@ #[AsInMemoryTest] final class InMemoryTest extends KernelTestCase { - use Factories; use RequiresORM; use ResetDatabase; diff --git a/tests/Integration/ResetDatabase/EarlyBootedKernelTest.php b/tests/Integration/ResetDatabase/EarlyBootedKernelTest.php index e37bbd6b4..d77a4f306 100644 --- a/tests/Integration/ResetDatabase/EarlyBootedKernelTest.php +++ b/tests/Integration/ResetDatabase/EarlyBootedKernelTest.php @@ -13,38 +13,12 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Driver\Middleware; -use PHPUnit\Framework\Attributes\BeforeClass; -use PHPUnit\Framework\Attributes\RequiresPhpunit; -use PHPUnit\Framework\Attributes\Test; -use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\DoctrineMiddleware; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; -/** - * @requires PHPUnit >=11.3 - */ -#[RequiresPhpunit('>=11.3')] -final class EarlyBootedKernelTest extends ResetDatabaseTestCase +#[ResetDatabase] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class EarlyBootedKernelTest extends EarlyBootedKernelTestCase { - /** - * Needs to happen before {@see ResetDatabase::_resetDatabaseBeforeFirstTest()}. - */ - #[BeforeClass(10)] - public static function before(): void - { - self::bootKernel(); - } - - #[Test] - public function connection_uses_doctrine_middleware(): void - { - /** @var Connection $connection */ - $connection = self::getContainer()->get(Connection::class); - - self::assertContains( - DoctrineMiddleware::class, - \array_map(static fn(Middleware $middleware) => $middleware::class, $connection->getConfiguration()->getMiddlewares()) - ); - } } diff --git a/tests/Integration/ResetDatabase/EarlyBootedKernelTestCase.php b/tests/Integration/ResetDatabase/EarlyBootedKernelTestCase.php new file mode 100644 index 000000000..903539589 --- /dev/null +++ b/tests/Integration/ResetDatabase/EarlyBootedKernelTestCase.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\Middleware; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\DoctrineMiddleware; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; + +abstract class EarlyBootedKernelTestCase extends KernelTestCase +{ + /** + * Needs to happen before {@see ResetDatabase::_resetDatabaseBeforeFirstTest()}. + */ + #[BeforeClass(10)] + public static function before(): void + { + self::bootKernel(); + } + + #[Test] + public function connection_uses_doctrine_middleware(): void + { + /** @var Connection $connection */ + $connection = self::getContainer()->get(Connection::class); + + self::assertContains( + DoctrineMiddleware::class, + \array_map(static fn(Middleware $middleware) => $middleware::class, $connection->getConfiguration()->getMiddlewares()) + ); + } + + protected static function getKernelClass(): string + { + return ResetDatabaseTestKernel::class; + } +} diff --git a/tests/Integration/ResetDatabase/EarlyBootedKernelWithTraitsTest.php b/tests/Integration/ResetDatabase/EarlyBootedKernelWithTraitsTest.php new file mode 100644 index 000000000..fa0981ccb --- /dev/null +++ b/tests/Integration/ResetDatabase/EarlyBootedKernelWithTraitsTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +#[IgnoreDeprecations('In order to use Foundry correctly, you must use the trait')] +final class EarlyBootedKernelWithTraitsTest extends EarlyBootedKernelTestCase +{ + use Factories, ResetDatabase; +} diff --git a/tests/Integration/ResetDatabase/GlobalStoryTest.php b/tests/Integration/ResetDatabase/GlobalStoryTest.php index 5e7f410b5..7564b98cd 100644 --- a/tests/Integration/ResetDatabase/GlobalStoryTest.php +++ b/tests/Integration/ResetDatabase/GlobalStoryTest.php @@ -13,7 +13,11 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; use Zenstruck\Foundry\Tests\Fixture\Document\GlobalDocument; use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; @@ -21,37 +25,8 @@ use function Zenstruck\Foundry\Persistence\repository; -final class GlobalStoryTest extends ResetDatabaseTestCase +#[ResetDatabase] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class GlobalStoryTest extends GlobalStoryTestCase { - /** - * @test - */ - #[Test] - public function global_stories_are_loaded(): void - { - if (FoundryTestKernel::hasORM()) { - repository(GlobalEntity::class)->assert()->count(2); - } - - if (FoundryTestKernel::hasMongo()) { - repository(GlobalDocument::class)->assert()->count(2); - } - } - - /** - * @test - */ - #[Test] - public function global_stories_cannot_be_loaded_again(): void - { - GlobalStory::load(); - - if (FoundryTestKernel::hasORM()) { - repository(GlobalEntity::class)->assert()->count(2); - } - - if (FoundryTestKernel::hasMongo()) { - repository(GlobalDocument::class)->assert()->count(2); - } - } } diff --git a/tests/Integration/ResetDatabase/GlobalStoryTestCase.php b/tests/Integration/ResetDatabase/GlobalStoryTestCase.php new file mode 100644 index 000000000..044118434 --- /dev/null +++ b/tests/Integration/ResetDatabase/GlobalStoryTestCase.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Tests\Fixture\Document\GlobalDocument; +use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; + +use function Zenstruck\Foundry\Persistence\repository; + +abstract class GlobalStoryTestCase extends KernelTestCase +{ + #[Test] + public function global_stories_are_loaded(): void + { + if (FoundryTestKernel::hasORM()) { + repository(GlobalEntity::class)->assert()->count(2); + } + + if (FoundryTestKernel::hasMongo()) { + repository(GlobalDocument::class)->assert()->count(2); + } + } + + #[Test] + public function global_stories_cannot_be_loaded_again(): void + { + GlobalStory::load(); + + if (FoundryTestKernel::hasORM()) { + repository(GlobalEntity::class)->assert()->count(2); + } + + if (FoundryTestKernel::hasMongo()) { + repository(GlobalDocument::class)->assert()->count(2); + } + } + + protected static function getKernelClass(): string + { + return ResetDatabaseTestKernel::class; + } +} diff --git a/tests/Integration/ResetDatabase/GlobalStoryWithTraitsTest.php b/tests/Integration/ResetDatabase/GlobalStoryWithTraitsTest.php new file mode 100644 index 000000000..6ca06984a --- /dev/null +++ b/tests/Integration/ResetDatabase/GlobalStoryWithTraitsTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +#[IgnoreDeprecations('In order to use Foundry correctly, you must use the trait')] +final class GlobalStoryWithTraitsTest extends GlobalStoryTestCase +{ + use Factories, ResetDatabase; +} diff --git a/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php index 1e7bf47e7..de96263ff 100644 --- a/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php @@ -13,49 +13,12 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\IgnorePhpunitWarnings; -use PHPUnit\Framework\Attributes\RequiresPhpunit; -use PHPUnit\Framework\Attributes\Test; -use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; -use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; -use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; -use Zenstruck\Foundry\Tests\Integration\ORM\EdgeCasesRelationshipTest; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use function Zenstruck\Foundry\Persistence\flush_after; -use function Zenstruck\Foundry\Persistence\persistent_factory; - -final class OrmEdgeCaseTest extends ResetDatabaseTestCase +#[ResetDatabase] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class OrmEdgeCaseTest extends OrmEdgeCaseTestCase { - use ChangesEntityRelationshipCascadePersist; - - /** @test */ - #[Test] - #[DataProvider('provideCascadeRelationshipsCombinations')] - #[UsingRelationships(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class, ['globalEntity'])] - #[RequiresPhpunit('>=11.4')] - #[IgnorePhpunitWarnings(EdgeCasesRelationshipTest::DATA_PROVIDER_WARNING_REGEX)] - public function it_can_use_flush_after_and_entity_from_global_state(): void - { - $relationshipWithGlobalEntityFactory = persistent_factory(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class); - $globalEntitiesCount = persistent_factory(GlobalEntity::class)::repository()->count(); - - flush_after(function() use ($relationshipWithGlobalEntityFactory) { - $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntityProxy()]); - $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntity()]); - }); - - // assert no extra GlobalEntity have been created - persistent_factory(GlobalEntity::class)::assert()->count($globalEntitiesCount); - - $relationshipWithGlobalEntityFactory::assert()->count(2); - - $entity = $relationshipWithGlobalEntityFactory::repository()->first(); - self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); - - $entity = $relationshipWithGlobalEntityFactory::repository()->last(); - self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); - } } diff --git a/tests/Integration/ResetDatabase/OrmEdgeCaseTestCase.php b/tests/Integration/ResetDatabase/OrmEdgeCaseTestCase.php new file mode 100644 index 000000000..e3a9e9a8d --- /dev/null +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseTestCase.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnorePhpunitWarnings; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; +use Zenstruck\Foundry\Tests\Integration\ORM\EdgeCasesRelationshipTest; + +use function Zenstruck\Foundry\Persistence\flush_after; +use function Zenstruck\Foundry\Persistence\persistent_factory; + +abstract class OrmEdgeCaseTestCase extends KernelTestCase +{ + use ChangesEntityRelationshipCascadePersist; + + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class, ['globalEntity'])] + #[RequiresPhpunit('>=11.4')] + #[IgnorePhpunitWarnings(EdgeCasesRelationshipTest::DATA_PROVIDER_WARNING_REGEX)] + public function it_can_use_flush_after_and_entity_from_global_state(): void + { + $relationshipWithGlobalEntityFactory = persistent_factory(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class); + $globalEntitiesCount = persistent_factory(GlobalEntity::class)::repository()->count(); + + flush_after(function() use ($relationshipWithGlobalEntityFactory) { + $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntityProxy()]); + $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntity()]); + }); + + // assert no extra GlobalEntity have been created + persistent_factory(GlobalEntity::class)::assert()->count($globalEntitiesCount); + + $relationshipWithGlobalEntityFactory::assert()->count(2); + + $entity = $relationshipWithGlobalEntityFactory::repository()->first(); + self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); + + $entity = $relationshipWithGlobalEntityFactory::repository()->last(); + self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); + } + + protected static function getKernelClass(): string + { + return ResetDatabaseTestKernel::class; + } +} diff --git a/tests/Integration/ResetDatabase/OrmEdgeCaseWithTraitsTest.php b/tests/Integration/ResetDatabase/OrmEdgeCaseWithTraitsTest.php new file mode 100644 index 000000000..caf79e8e2 --- /dev/null +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseWithTraitsTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +#[IgnoreDeprecations('In order to use Foundry correctly, you must use the trait')] +final class OrmEdgeCaseWithTraitsTest extends OrmEdgeCaseTestCase +{ + use Factories, ResetDatabase; +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php index 0377ef93f..802c48f3d 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTest.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -13,156 +13,15 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; -use PHPUnit\Framework\Attributes\Depends; -use PHPUnit\Framework\Attributes\Test; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\BufferedOutput; -use Zenstruck\Foundry\Persistence\PersistenceManager; -use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; -use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; -use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; -use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\MongoResetterDecorator; -use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; - -use function Zenstruck\Foundry\Persistence\persist; -use function Zenstruck\Foundry\Persistence\repository; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Nicolas PHILIPPE */ +#[ResetDatabase] +#[RequiresPhpunitExtension(FoundryExtension::class)] final class ResetDatabaseTest extends ResetDatabaseTestCase { - /** - * @test - */ - #[Test] - public function it_generates_valid_schema(): void - { - $application = new Application(self::bootKernel()); - $application->setAutoExit(false); - - $exit = $application->run( - new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), - $output = new BufferedOutput() - ); - - if (FoundryTestKernel::usesMigrations()) { - // The command actually fails, because of a bug in doctrine ORM 3! - // https://github.com/doctrine/migrations/issues/1406 - self::assertSame(2, $exit, \sprintf('Schema is not valid: %s', $commandOutput = $output->fetch())); - self::assertStringContainsString('1 schema diff(s) detected', $commandOutput); - self::assertStringContainsString('DROP TABLE doctrine_migration_versions', $commandOutput); - } else { - self::assertSame(0, $exit, \sprintf('Schema is not valid: %s', $output->fetch())); - } - } - - /** - * @test - */ - #[Test] - public function it_can_store_object(): void - { - if (FoundryTestKernel::hasORM()) { - GenericEntityFactory::assert()->count(0); - GenericEntityFactory::createOne(); - GenericEntityFactory::assert()->count(1); - } - - if (FoundryTestKernel::hasMongo()) { - GenericDocumentFactory::assert()->count(0); - GenericDocumentFactory::createOne(); - GenericDocumentFactory::assert()->count(1); - } - } - - /** - * @test - * @depends it_can_store_object - */ - #[Test] - #[Depends('it_can_store_object')] - public function it_still_starts_from_fresh_db(): void - { - if (FoundryTestKernel::hasORM()) { - GenericEntityFactory::assert()->count(0); - } - - if (FoundryTestKernel::hasMongo()) { - GenericDocumentFactory::assert()->count(0); - } - } - - /** - * @test - */ - #[Test] - public function can_create_object_in_another_schema(): void - { - if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { - self::markTestSkipped('PostgreSQL needed.'); - } - - persist(Article::class, ['title' => 'Hello World!']); - repository(Article::class)->assert()->count(1); - } - - /** - * @test - */ - #[Test] - public function can_extend_orm_reset_mechanism_first(): void - { - if (!FoundryTestKernel::hasORM()) { - self::markTestSkipped('ORM needed.'); - } - - self::assertTrue(OrmResetterDecorator::$calledBeforeFirstTest); - - if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { - // in this case, the resetBeforeEachTest() method is never called - self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); - } else { - self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); - } - - OrmResetterDecorator::reset(); - } - - /** - * @test - * @depends can_extend_orm_reset_mechanism_first - */ - #[Test] - #[Depends('can_extend_orm_reset_mechanism_first')] - public function can_extend_orm_reset_mechanism_second(): void - { - if (!FoundryTestKernel::hasORM()) { - self::markTestSkipped('ORM needed.'); - } - - self::assertFalse(OrmResetterDecorator::$calledBeforeFirstTest); - - if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { - // in this case, the resetBeforeEachTest() method is never called - self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); - } else { - self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); - } - } - - /** - * @test - */ - #[Test] - public function can_extend_mongo_reset_mechanism_first(): void - { - if (!FoundryTestKernel::hasMongo()) { - self::markTestSkipped('Mongo needed.'); - } - - self::assertTrue(MongoResetterDecorator::$calledBeforeEachTest); - } } diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php index f09625cab..2397b4f57 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php @@ -13,14 +13,136 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; +use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Zenstruck\Foundry\Attribute\ResetDatabase; +use Zenstruck\Foundry\Persistence\PersistenceManager; use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Test\ResetDatabase as ResetDatabaseTrait; +use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\MongoResetterDecorator; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; +use function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\repository; abstract class ResetDatabaseTestCase extends KernelTestCase { - use Factories, ResetDatabase; + #[Test] + public function it_generates_valid_schema(): void + { + $application = new Application(self::bootKernel()); + $application->setAutoExit(false); + + $exit = $application->run( + new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), + $output = new BufferedOutput() + ); + + if (FoundryTestKernel::usesMigrations()) { + // The command actually fails, because of a bug in doctrine ORM 3! + // https://github.com/doctrine/migrations/issues/1406 + self::assertSame(2, $exit, \sprintf('Schema is not valid: %s', $commandOutput = $output->fetch())); + self::assertStringContainsString('1 schema diff(s) detected', $commandOutput); + self::assertStringContainsString('DROP TABLE doctrine_migration_versions', $commandOutput); + } else { + self::assertSame(0, $exit, \sprintf('Schema is not valid: %s', $output->fetch())); + } + } + + #[Test] + public function it_can_store_object(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + GenericEntityFactory::createOne(); + GenericEntityFactory::assert()->count(1); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + GenericDocumentFactory::createOne(); + GenericDocumentFactory::assert()->count(1); + } + } + + #[Test] + #[Depends('it_can_store_object')] + public function it_still_starts_from_fresh_db(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + } + } + + #[Test] + public function can_create_object_in_another_schema(): void + { + if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { + self::markTestSkipped('PostgreSQL needed.'); + } + + persist(Article::class, ['title' => 'Hello World!']); + repository(Article::class)->assert()->count(1); + } + + #[Test] + public function can_extend_orm_reset_mechanism_first(): void + { + if (!FoundryTestKernel::hasORM()) { + self::markTestSkipped('ORM needed.'); + } + + self::assertTrue(OrmResetterDecorator::$calledBeforeFirstTest); + + if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { + // in this case, the resetBeforeEachTest() method is never called + self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); + } else { + self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); + } + + OrmResetterDecorator::reset(); + } + + #[Test] + #[Depends('can_extend_orm_reset_mechanism_first')] + public function can_extend_orm_reset_mechanism_second(): void + { + if (!FoundryTestKernel::hasORM()) { + self::markTestSkipped('ORM needed.'); + } + + self::assertFalse(OrmResetterDecorator::$calledBeforeFirstTest); + + if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { + // in this case, the resetBeforeEachTest() method is never called + self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); + } else { + self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); + } + } + + #[Test] + public function can_extend_mongo_reset_mechanism_first(): void + { + if (!FoundryTestKernel::hasMongo()) { + self::markTestSkipped('Mongo needed.'); + } + + self::assertTrue(MongoResetterDecorator::$calledBeforeEachTest); + } protected static function getKernelClass(): string { diff --git a/tests/Integration/ResetDatabase/ResetDatabaseWithTraitsTest.php b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitsTest.php new file mode 100644 index 000000000..3da9239c3 --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitsTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +/** + * @author Nicolas PHILIPPE + */ +#[IgnoreDeprecations('In order to use Foundry correctly, you must use the trait')] +final class ResetDatabaseWithTraitsTest extends ResetDatabaseTestCase +{ + use Factories, ResetDatabase; + + #[BeforeClass(10)] + public static function skipIfExtensionEnabled(): void + { + if (FoundryExtension::isEnabled()) { + self::markTestSkipped('FoundryExtension is enabled.'); + } + } +} diff --git a/utils/rector/config/foundry-2.10.php b/utils/rector/config/foundry-2.10.php new file mode 100644 index 000000000..93e40bbd6 --- /dev/null +++ b/utils/rector/config/foundry-2.10.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ResetDatabaseAttributeRector; + +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->rule(ResetDatabaseAttributeRector::class); +}; diff --git a/utils/rector/config/foundry-set.php b/utils/rector/config/foundry-2.7.php similarity index 100% rename from utils/rector/config/foundry-set.php rename to utils/rector/config/foundry-2.7.php diff --git a/utils/rector/config/foundry-2.9.php b/utils/rector/config/foundry-2.9.php new file mode 100644 index 000000000..99626b9db --- /dev/null +++ b/utils/rector/config/foundry-2.9.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Rector\Removing\Rector\Class_\RemoveTraitUseRector; +use Zenstruck\Foundry\Test\Factories; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->ruleWithConfiguration( + RemoveTraitUseRector::class, + [ + Factories::class, + ] + ); +}; diff --git a/utils/rector/src/FoundrySetList.php b/utils/rector/src/FoundrySetList.php index 86f51cf37..ad4d6af2c 100644 --- a/utils/rector/src/FoundrySetList.php +++ b/utils/rector/src/FoundrySetList.php @@ -13,6 +13,18 @@ final class FoundrySetList { + /** + * @deprecated use FoundrySetList::FOUNDRY_2_7 + * @var string + */ + public const REMOVE_PROXIES = self::FOUNDRY_2_7; + + /** @var string */ + public const FOUNDRY_2_7 = __DIR__.'/../config/foundry-2.7.php'; + + /** @var string */ + public const FOUNDRY_2_9 = __DIR__.'/../config/foundry-2.9.php'; + /** @var string */ - public const REMOVE_PROXIES = __DIR__.'/../config/foundry-set.php'; + public const FOUNDRY_2_10 = __DIR__.'/../config/foundry-2.10.php'; } diff --git a/utils/rector/src/ResetDatabaseAttributeRector.php b/utils/rector/src/ResetDatabaseAttributeRector.php new file mode 100644 index 000000000..d1010432e --- /dev/null +++ b/utils/rector/src/ResetDatabaseAttributeRector.php @@ -0,0 +1,60 @@ +> */ + public function getNodeTypes(): array + { + return [Node\Stmt\Class_::class]; + } + + /** @param Node\Stmt\Class_ $node */ + public function refactor(Node $node): Node|null + { + /** @var ?Node\Stmt\TraitUse $traitUseWithResetDatabase */ + $traitUseWithResetDatabase = $this->nodeFinder->findFirst($node->stmts, function (Node $node): bool { + return $node instanceof Node\Stmt\TraitUse + && array_any($node->traits, fn(Node\Name $name) => $this->getName($name) === ResetDatabaseTrait::class); + }); + + if (!$traitUseWithResetDatabase) { + return null; + } + + $traitUseWithResetDatabase->traits = array_filter( + $traitUseWithResetDatabase->traits, + fn(Node\Name $name) => $this->getName($name) !== ResetDatabaseTrait::class + ); + + if ($traitUseWithResetDatabase->traits === []) { + $node->stmts = array_filter($node->stmts, fn(Node\Stmt $stmt) => $stmt !== $traitUseWithResetDatabase); + } + + $hasResetDatabaseTrait = (bool)$this->nodeFinder->findFirst($node->attrGroups, function (Node $node): bool { + return $this->getName($node) === ResetDatabaseAttribute::class; + }); + + if ($hasResetDatabaseTrait) { + return $node; + } + + $node->attrGroups[] = new Node\AttributeGroup([ + new Node\Attribute(new Node\Name\FullyQualified(ResetDatabaseAttribute::class)), + ]); + + return $node; + } +} diff --git a/utils/rector/tests/AllRules/AllRulesTest.php b/utils/rector/tests/AllRules/AllRulesTest.php index 4a8ad451b..c0bdda4fa 100644 --- a/utils/rector/tests/AllRules/AllRulesTest.php +++ b/utils/rector/tests/AllRules/AllRulesTest.php @@ -36,6 +36,6 @@ public static function provideData(): \Iterator public function provideConfigFilePath(): string { - return __DIR__.'/../../config/foundry-set.php'; + return __DIR__.'/../../config/foundry-2.7.php'; } } diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc new file mode 100644 index 000000000..d1bc2afbd --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc new file mode 100644 index 000000000..8cf92c884 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc new file mode 100644 index 000000000..73717ea3d --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc @@ -0,0 +1,9 @@ + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc new file mode 100644 index 000000000..fe3d039d8 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc @@ -0,0 +1,24 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc new file mode 100644 index 000000000..f9b30024f --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc @@ -0,0 +1,47 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php b/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php new file mode 100644 index 000000000..178ea4a75 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ResetDatabaseAttribute; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ResetDatabaseAttributeRectorTest extends AbstractRectorTestCase +{ + #[Test] + #[DataProvider('provideData')] + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ResetDatabaseAttribute/config.php b/utils/rector/tests/ResetDatabaseAttribute/config.php new file mode 100644 index 000000000..0fae53c07 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/config.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ResetDatabaseAttributeRector; + +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->rules( + [ResetDatabaseAttributeRector::class], + ); +};