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/ci.yml b/.github/workflows/ci.yml index adc87dca8..ca1f237c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,7 @@ jobs: 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 + USE_FOUNDRY_PHPUNIT_EXTENSION: 1 services: postgres: image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} @@ -189,7 +190,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.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.8.md b/UPGRADE-2.8.md new file mode 100644 index 000000000..cca974d71 --- /dev/null +++ b/UPGRADE-2.8.md @@ -0,0 +1,53 @@ +# Migration guide from Foundry 2.7 to 2.8 + +The main feature of Foundry 2.8 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_8]) +; +``` diff --git a/UPGRADE-2.9.md b/UPGRADE-2.9.md new file mode 100644 index 000000000..58e408b98 --- /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 `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_9]) +; +``` diff --git a/docs/index.rst b/docs/index.rst index 1355a89c6..4c4ff6d03 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1606,59 +1606,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.8 + + The ability to globally enable Foundry with PHPUnit extension was introduced in Foundry 2.8 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 @@ -1780,7 +1811,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 { @@ -2030,7 +2061,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:: @@ -2053,7 +2084,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:: @@ -2249,7 +2280,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. :: @@ -2378,8 +2409,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(); @@ -2596,19 +2625,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: @@ -2618,7 +2647,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 c79993e27..4d6dc0903 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 56e3635af..5448ab66e 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -7,13 +7,20 @@ + + + - + + 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 d531f27cd..55c3f4267 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -20,6 +20,7 @@ use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond @@ -117,7 +118,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; } @@ -132,6 +135,10 @@ public static function boot(\Closure|self $configuration): void { PersistedObjectsTracker::reset(); self::$instance = $configuration; + + if (FoundryExtension::shouldBeEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.8', '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.8.md to upgrade.'); + } } /** @param \Closure():self|self $configuration */ diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php index 5f7fbef27..d6fa3fc8c 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::isEnabled() + ? '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/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..c94209151 --- /dev/null +++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php @@ -0,0 +1,73 @@ + + * + * 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\Persistence\PersistentObjectFromDataProviderRegistry; +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()); + + PersistentObjectFromDataProviderRegistry::instance()->addDataset( + $event->testMethod()->className(), + $event->testMethod()->methodName(), + "{$event->dataProviderMethod()->className()}::{$event->dataProviderMethod()->methodName()}"(...) // @phpstan-ignore callable.nonCallable + ); + + $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/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php similarity index 69% rename from src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php rename to src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php index b028394b3..690ecfa7f 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php @@ -11,9 +11,11 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\PHPUnit; +namespace Zenstruck\Foundry\PHPUnit\DataProvider; use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper; /** * @internal @@ -23,8 +25,8 @@ final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\Da { public function notify(Event\Test\DataProviderMethodFinished $event): void { - if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { - $event->testMethod()->className()::_shutdownAfterDataProvider(); - } + 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..50fe7ecfd --- /dev/null +++ b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.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\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..d8e6c79c8 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 + { + return false; } - $facade->registerSubscribers(...$subscribers); + public static function isEnabled(): bool + { + 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/ShutdownFoundryOnTestFinished.php b/src/PHPUnit/ShutdownFoundryOnTestFinished.php new file mode 100644 index 000000000..bfa9b99fb --- /dev/null +++ b/src/PHPUnit/ShutdownFoundryOnTestFinished.php @@ -0,0 +1,29 @@ + + * + * 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; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ShutdownFoundryOnTestFinished implements Event\Test\FinishedSubscriber +{ + public function notify(Event\Test\Finished $event): void + { + Configuration::shutdown(); + } +} diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 411e5a268..58e7ee697 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -245,7 +245,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..740e21e4d --- /dev/null +++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +/** + * If a persistent object has been created in a data provider, we need to initialize the proxy object, + * which will trigger the object to be persisted. + * + * Otherwise, such 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! + * We collect all the "datasets" and we trigger the persistence for each one before the test is executed. + * This means that de data providers 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 $shouldReturnExistingObject = false; + + public static function instance(): self + { + return self::$instance ?? self::$instance = new self(); + } + + /** + * @param callable():iterable $dataProviderResult + */ + public function addDataset(string $className, string $methodName, callable $dataProviderResult): void + { + $this->shouldReturnExistingObject = false; + + $dataProviderResult = $dataProviderResult(); + + if (!\is_array($dataProviderResult)) { + $dataProviderResult = \iterator_to_array($dataProviderResult); + } + + $testCaseContext = $this->testCaseContext($className, $methodName); + $this->datasets[$testCaseContext] = $dataProviderResult; + + $this->shouldReturnExistingObject = true; + } + + /** + * @template T of object + * + * @param PersistentObjectFactory $factory + * + * @return ($factory is PersistentProxyObjectFactory ? T&Proxy : T) + */ + public function deferObjectCreation(PersistentObjectFactory $factory): object + { + if (!$this->shouldReturnExistingObject) { + return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory); + } + + 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])) { + throw new \LogicException("No data found for test case context \"{$testCaseContext}\" with dataset name \"{$dataSetName}\"."); + } + + initialize_proxy_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 b8d43b007..0b0f3f94f 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) { // @phpstan-ignore-line + return $reflector->newLazyProxy(static function() use ($factory) { // @phpstan-ignore-line 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/Test/Factories.php b/src/Test/Factories.php index 3b7436a2d..ebae219ed 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -15,6 +15,7 @@ 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; @@ -27,9 +28,15 @@ trait Factories * @internal * @before */ - #[Before] + #[Before(5)] public function _beforeHook(): void { + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.8', \sprintf('Trait %s is deprecated and will be removed in Foundry 3. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.8.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,7 +105,9 @@ private function _loadDataProvidedProxies(): void return; } - $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound + $providedData = \method_exists($this, 'getProvidedData') // @phpstan-ignore function.impossibleType + ? $this->getProvidedData() // @phpstan-ignore method.notFound + : $this->providedData(); initialize_proxy_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/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php index f4d28b26f..0f7a16f5e 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/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php index 1a6f07194..d37e9bfb5 100644 --- a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php +++ b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php @@ -30,6 +30,8 @@ use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; +use function Zenstruck\Foundry\Persistence\assert_persisted; + /** * @author Nicolas PHILIPPE * @requires PHPUnit >=11.4 @@ -51,6 +53,9 @@ public function assert_it_can_create_one_object_in_data_provider(?GenericModel $ 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()); + + static::proxyFactory()::assert()->count(1); + $providedData->_assertPersisted(); } public static function createOneProxyObjectInDataProvider(): iterable @@ -136,20 +141,24 @@ public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterab #[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); + static::factory()::assert()->count(1); + self::assertNotInstanceOf(Proxy::class, $providedData); self::assertInstanceOf(GenericModel::class, $providedData); self::assertSame('value set in data provider', $providedData->getProp1()); + + assert_persisted($providedData); + static::factory()::assert()->count(1); } public static function createOneObjectInDataProvider(): iterable { yield 'createOne()' => [ - static::proxyFactory()::createOne(['prop1' => 'value set in data provider']), + static::factory()::createOne(['prop1' => 'value set in data provider']), ]; yield 'create()' => [ - static::proxyFactory()->create(['prop1' => 'value set in data provider']), + static::factory()->create(['prop1' => 'value set in data provider']), ]; } diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentFactoryTest.php similarity index 92% rename from tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php rename to tests/Integration/DataProvider/GenericDocumentFactoryTest.php index f18c5136b..34c0f607a 100644 --- a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericDocumentFactoryTest.php @@ -27,7 +27,7 @@ #[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[IgnoreDeprecations] -final class GenericDocumentProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase +final class GenericDocumentFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase { use RequiresMongo; diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityFactoryTest.php similarity index 92% rename from tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php rename to tests/Integration/DataProvider/GenericEntityFactoryTest.php index 1f5eadb08..aa3412575 100644 --- a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericEntityFactoryTest.php @@ -27,7 +27,7 @@ #[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[IgnoreDeprecations] -final class GenericEntityProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase +final class GenericEntityFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase { use RequiresORM; 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/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php index 0377ef93f..7ba1643e2 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTest.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -15,99 +15,17 @@ 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; - /** * @author Nicolas PHILIPPE */ 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); - } + use ResetDatabaseTestsTrait; /** * @test diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php index f09625cab..7f22b0867 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php @@ -14,14 +14,12 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Attribute\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; +#[ResetDatabase] abstract class ResetDatabaseTestCase extends KernelTestCase { - use Factories, ResetDatabase; - protected static function getKernelClass(): string { return ResetDatabaseTestKernel::class; diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php b/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php new file mode 100644 index 000000000..eb25dde44 --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php @@ -0,0 +1,105 @@ + + * + * 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\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\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 function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\repository; + +trait ResetDatabaseTestsTrait +{ + /** + * @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); + } +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php new file mode 100644 index 000000000..75d074caa --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php @@ -0,0 +1,25 @@ + + * + * 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 Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\ResetDatabase; + +/** + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseWithTraitTest extends KernelTestCase +{ + use ResetDatabase, ResetDatabaseTestsTrait; +} 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.8.php b/utils/rector/config/foundry-2.8.php new file mode 100644 index 000000000..5f505c91d --- /dev/null +++ b/utils/rector/config/foundry-2.8.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/config/foundry-2.9.php b/utils/rector/config/foundry-2.9.php new file mode 100644 index 000000000..5f505c91d --- /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..d406e41c2 100644 --- a/utils/rector/src/FoundrySetList.php +++ b/utils/rector/src/FoundrySetList.php @@ -13,6 +13,15 @@ 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 REMOVE_PROXIES = __DIR__.'/../config/foundry-set.php'; + public const FOUNDRY_2_8 = __DIR__.'/../config/foundry-2.8.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 2fdcdb935..520891d12 100644 --- a/utils/rector/tests/AllRules/AllRulesTest.php +++ b/utils/rector/tests/AllRules/AllRulesTest.php @@ -24,6 +24,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], + ); +};