diff --git a/.env b/.env index da52e373d..d443158a7 100644 --- a/.env +++ b/.env @@ -2,4 +2,5 @@ DATABASE_URL="mysql://root:1234@127.0.0.1:3307/foundry_test?serverVersion=5.7.42 MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServiceName=mongodb" DATABASE_RESET_MODE="schema" USE_DAMA_DOCTRINE_TEST_BUNDLE="0" +USE_FOUNDRY_PHPUNIT_EXTENSION="0" PHPUNIT_VERSION="9" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5df09b069..b3b007383 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }} + name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -19,6 +19,7 @@ jobs: database: [ mysql, mongo ] use-dama: [ 1 ] use-migrate: [ 0 ] + use-phpunit-extension: [ 0 ] phpunit: [ 9 ] exclude: - php: 8.1 @@ -32,6 +33,7 @@ jobs: database: none use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -39,6 +41,7 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -46,6 +49,7 @@ jobs: database: pgsql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -53,6 +57,7 @@ jobs: database: pgsql use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -60,6 +65,7 @@ jobs: database: sqlite use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: lowest @@ -67,6 +73,7 @@ jobs: database: sqlite use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: lowest @@ -74,6 +81,7 @@ jobs: database: mysql use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -81,6 +89,7 @@ jobs: database: mysql use-dama: 1 use-migrate: 1 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -88,6 +97,7 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 10 - php: 8.3 deps: highest @@ -95,11 +105,21 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 11 + - php: 8.3 + deps: highest + symfony: '*' + database: mysql|mongo + use-dama: 1 + use-migrate: 0 + use-phpunit-extension: 1 + phpunit: 11.4 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' || contains(matrix.database, 'sqlite') && '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 && contains(matrix.database, 'sql') && 1 || 0 }} + USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension }} PHPUNIT_VERSION: ${{ matrix.phpunit }} services: postgres: @@ -155,7 +175,8 @@ jobs: DATABASE_URL: postgresql://root:root@localhost:5432/foundry?serverVersion=15 MONGO_URL: mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb USE_DAMA_DOCTRINE_TEST_BUNDLE: 1 - PHPUNIT_VERSION: 9 + USE_FOUNDRY_PHPUNIT_EXTENSION: 1 + PHPUNIT_VERSION: 11.4 services: mongo: image: mongo:4 diff --git a/bin/console b/bin/console index 5944a814f..9ff51b3b3 100755 --- a/bin/console +++ b/bin/console @@ -2,9 +2,9 @@ run(); diff --git a/phpstan.neon b/phpstan.neon index 030b3b5e3..25e84ea2f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,6 +14,10 @@ parameters: - identifier: missingType.iterableValue path: tests/ + # We support both PHPUnit versions (this method changed in PHPUnit 10) + - message: '#Call to function method_exists\(\) with .* will always evaluate to false#' + path: src/Test/Factories.php + excludePaths: - tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php - tests/Fixture/Maker/expected/can_create_factory_interactively.php diff --git a/phpunit b/phpunit index 7de0be16c..01da63180 100755 --- a/phpunit +++ b/phpunit @@ -49,21 +49,21 @@ if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then fi ### << -### >> guess extensions -EXTENSION="" +### >> actually execute PHPUnit with the right options +DAMA_EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +FOUNDRY_EXTENSION="Zenstruck\Foundry\Test\PHPUnit\Extension" -if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then - EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" != "11.4" ]; then + echo "❌ USE_FOUNDRY_PHPUNIT_EXTENSION could only be used with PHPUNIT_VERSION=11.4"; + exit 1; fi -### << -### >> actually execute PHPUnit with the right options case ${PHPUNIT_VERSION} in "9") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit.xml.dist "$@" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + vendor/bin/phpunit -c phpunit.xml.dist --extensions "${DAMA_EXTENSION}" "$@" else - vendor/bin/phpunit -c phpunit.xml.dist --extensions "${EXTENSION}" "$@" + vendor/bin/phpunit -c phpunit.xml.dist "$@" fi ;; @@ -73,11 +73,16 @@ case ${PHPUNIT_VERSION} in ;; "11"|"11.4") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit-10.xml.dist "$@" - else - vendor/bin/phpunit -c phpunit-10.xml.dist --extension "${EXTENSION}" "$@" + PHPUNIT_EXEC="vendor/bin/phpunit -c phpunit-10.xml.dist $@" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${DAMA_EXTENSION}"" fi + + if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${FOUNDRY_EXTENSION}"" + fi + + $PHPUNIT_EXEC ;; esac ### << diff --git a/phpunit-10.xml.dist b/phpunit-10.xml.dist index 1727f9b8b..d4d9428a2 100644 --- a/phpunit-10.xml.dist +++ b/phpunit-10.xml.dist @@ -1,7 +1,7 @@ bootedForDataProvider; + } + public static function instance(): self { if (!self::$instance) { - throw new FoundryNotBooted('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + throw new FoundryNotBooted(); } - return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; + return self::$instance; } public static function isBooted(): bool @@ -82,7 +91,14 @@ public static function isBooted(): bool public static function boot(\Closure|self $configuration): void { - self::$instance = $configuration; + self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration; + self::$instance->bootedForDataProvider = false; + } + + public static function bootForDataProvider(\Closure|self $configuration): void + { + self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration; + self::$instance->bootedForDataProvider = true; } public static function shutdown(): void diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php index e4b04e697..5f7fbef27 100644 --- a/src/Exception/FoundryNotBooted.php +++ b/src/Exception/FoundryNotBooted.php @@ -16,4 +16,8 @@ */ 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.'); + } } diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index 044af5ac1..a49967f6b 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Persistence; -use Doctrine\ODM\MongoDB\DocumentManager; use Symfony\Component\VarExporter\LazyProxyTrait; use Zenstruck\Assert; use Zenstruck\Foundry\Configuration; @@ -128,6 +127,11 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but return $this; } + public function _initializeLazyObject(): void + { + $this->initializeLazyObject(); + } + private function isPersisted(): bool { try { diff --git a/src/Persistence/PersistenceStrategy.php b/src/Persistence/PersistenceStrategy.php index 688105db0..f9ed49bd4 100644 --- a/src/Persistence/PersistenceStrategy.php +++ b/src/Persistence/PersistenceStrategy.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Persistence; -use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\MappingException; diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 63f723d5e..7fb4d8787 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -204,12 +204,15 @@ final public static function truncate(): void } /** + * @final * @return T */ - final public function create(callable|array $attributes = []): object + public function create(callable|array $attributes = []): object { $object = parent::create($attributes); + $this->throwIfCannotCreateObject(); + if (!$this->isPersisting()) { return $this->proxy($object); } @@ -361,4 +364,26 @@ private function proxy(object $object): object return $this->isPersisting() ? $object : $object->_disableAutoRefresh(); } + + private function throwIfCannotCreateObject(): void + { + $configuration = Configuration::instance(); + + /** + * "false === $configuration->inADataProvider()" would also mean that the PHPUnit extension is NOT used + * so a `FoundryNotBooted` exception would be thrown if we actually are in a data provider. + */ + if (!$configuration->inADataProvider()) { + return; + } + + if ( + !$configuration->isPersistenceAvailable() + || $this instanceof PersistentProxyObjectFactory + ) { + return; + } + + throw new \LogicException(\sprintf('Cannot create object in a data provider for non-proxy factories. Transform your factory into a "%s", or call "create()" method in the test. See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-data-providers', PersistentProxyObjectFactory::class)); + } } diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index a8b4870cb..1d42075cc 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -14,8 +14,8 @@ use Doctrine\Persistence\ObjectRepository; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\FactoryCollection; // keep me! +use Zenstruck\Foundry\Object\Instantiator; /** * @author Kevin Bond @@ -43,6 +43,19 @@ */ abstract class PersistentProxyObjectFactory extends PersistentObjectFactory { + /** + * @return T&Proxy + */ + final public function create(callable|array $attributes = []): object + { + $configuration = Configuration::instance(); + if ($configuration->inADataProvider()) { + return ProxyGenerator::wrapFactory($this, $attributes); + } + + return parent::create($attributes); + } + /** * @return class-string */ diff --git a/src/Persistence/Proxy.php b/src/Persistence/Proxy.php index cbddf6af7..57a8d9ce6 100644 --- a/src/Persistence/Proxy.php +++ b/src/Persistence/Proxy.php @@ -53,4 +53,9 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but * @return ProxyRepositoryDecorator> */ public function _repository(): ProxyRepositoryDecorator; + + /** + * @internal + */ + public function _initializeLazyObject(): void; } diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index 2458426b9..aedeb7d51 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -15,11 +15,14 @@ use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; +use Zenstruck\Foundry\Factory; /** * @author Kevin Bond * * @internal + * + * @phpstan-import-type Attributes from Factory */ final class ProxyGenerator { @@ -43,6 +46,19 @@ public static function wrap(object $object): Proxy return self::generateClassFor($object)::createLazyProxy(static fn() => $object); // @phpstan-ignore-line } + /** + * @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 fn() => unproxy($factory->create($attributes))); // @phpstan-ignore-line + } + /** * @template T * @@ -67,6 +83,14 @@ public static function unwrap(mixed $what): mixed return $what; } + /** + * @param class-string $class + */ + public static function proxyClassNameFor(string $class): string + { + return \str_replace('\\', '', $class).'Proxy'; + } + /** * @template T of object * @@ -76,8 +100,8 @@ public static function unwrap(mixed $what): mixed */ private static function generateClassFor(object $object): string { - /** @var class-string $class */ - $class = $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; + $class = self::extractClassName($object); + $proxyClass = self::proxyClassNameFor($class); /** @var class-string&T> $proxyClass */ @@ -102,10 +126,14 @@ private static function generateClassFor(object $object): string } /** - * @param class-string $class + * @return class-string */ - public static function proxyClassNameFor(string $class): string + private static function extractClassName(object $object): string { - return \str_replace('\\', '', $class).'Proxy'; + if ($object instanceof PersistentProxyObjectFactory) { + return $object::class(); + } + + return $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; // @phpstan-ignore return.type } } diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index 69afbbf8c..3e82b1d09 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -171,3 +171,15 @@ function enable_persisting(): void { Configuration::instance()->persistence()->enablePersisting(); } + +/** + * @internal + */ +function initialize_proxy_object(mixed $what): void +{ + match (true) { + $what instanceof Proxy => $what->_initializeLazyObject(), + \is_array($what) => \array_map(initialize_proxy_object(...), $what), + default => true, // do nothing + }; +} diff --git a/src/Test/Factories.php b/src/Test/Factories.php index 7c22831b3..8bc6c1e1a 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -16,6 +16,8 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; +use function Zenstruck\Foundry\Persistence\initialize_proxy_object; + /** * @author Kevin Bond */ @@ -26,7 +28,26 @@ trait Factories * @before */ #[Before] - public static function _bootFoundry(): void + public function _beforeHook(): void + { + $this->_bootFoundry(); + $this->_loadDataProvidedProxies(); + } + + /** + * @internal + * @after + */ + #[After] + public static function _shutdownFoundry(): void + { + Configuration::shutdown(); + } + + /** + * @internal + */ + private function _bootFoundry(): void { if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore-line // unit test @@ -47,12 +68,35 @@ public static function _bootFoundry(): void } /** + * 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 in a subscriber, since PHPUnit does not give access to the actual tests instances. + * * @internal - * @after */ - #[After] - public static function _shutdownFoundry(): void + private function _loadDataProvidedProxies(): void { - Configuration::shutdown(); + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore-line + return; + } + + $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound + + initialize_proxy_object($providedData); } } diff --git a/src/Test/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/Test/PHPUnit/BootFoundryOnDataProviderMethodCalled.php new file mode 100644 index 000000000..2178bef99 --- /dev/null +++ b/src/Test/PHPUnit/BootFoundryOnDataProviderMethodCalled.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\Test\PHPUnit; + +use PHPUnit\Event; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpKernel\KernelInterface; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Test\UnitTestConfig; + +/** + * @author Nicolas PHILIPPE + */ +final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber +{ + public function __construct( + private KernelInterface $kernel, + ) { + } + + public function notify(Event\Test\DataProviderMethodCalled $event): void + { + if (\is_a($event->testMethod()->className(), KernelTestCase::class, allow_string: true)) { + static $kernelIsBooted = false; + + if (!$kernelIsBooted) { + $this->kernel->boot(); + $kernelIsBooted = true; + } + + Configuration::bootForDataProvider( + fn() => $this->kernel->getContainer()->get('.zenstruck_foundry.configuration'), + ); + } else { + Configuration::bootForDataProvider(UnitTestConfig::build()); + } + } +} diff --git a/src/Test/PHPUnit/Extension.php b/src/Test/PHPUnit/Extension.php new file mode 100644 index 000000000..4e7632a28 --- /dev/null +++ b/src/Test/PHPUnit/Extension.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Test\PHPUnit; + +use PHPUnit\Metadata\Version\ConstraintRequirement; +use PHPUnit\Runner; +use PHPUnit\TextUI; +use Symfony\Component\HttpKernel\KernelInterface; +use Zenstruck\Foundry\Configuration; + +final class Extension implements Runner\Extension\Extension +{ + public const MIN_PHPUNIT_VERSION = '11.4'; + + public function bootstrap( + TextUI\Configuration\Configuration $configuration, + Runner\Extension\Facade $facade, + Runner\Extension\ParameterCollection $parameters, + ): void { + if (!ConstraintRequirement::from(self::MIN_PHPUNIT_VERSION)->isSatisfiedBy(Runner\Version::id())) { + throw new \LogicException(\sprintf('Your PHPUnit version (%s) is not compatible with the minimum version (%s) needed to use this extension.', Runner\Version::id(), self::MIN_PHPUNIT_VERSION)); + } + + // shutdown Foundry if for some reason it has been booted before + if (Configuration::isBooted()) { + Configuration::shutdown(); + } + + // todo: should we deal with different options passed to createKernel/bootKernel? + $kernel = $this->createKernel(); + + $facade->registerSubscribers( + new BootFoundryOnDataProviderMethodCalled($kernel), + new ShutdownKernelOnTestSuiteLoaded($kernel), + ); + } + + /** + * This logic was shamelessly stolen from Symfony's KernelTestCase. + */ + private function createKernel(): KernelInterface + { + if (!isset($_SERVER['KERNEL_CLASS']) && !isset($_ENV['KERNEL_CLASS'])) { + throw new \LogicException('You must set the KERNEL_CLASS environment variable to the fully-qualified class name of your Kernel in phpunit.xml / phpunit.xml.dist.'); + } + + if (!\class_exists($class = $_ENV['KERNEL_CLASS'] ?? $_SERVER['KERNEL_CLASS'])) { + throw new \RuntimeException(\sprintf('Class "%s" doesn\'t exist or cannot be autoloaded. Check that the KERNEL_CLASS value in phpunit.xml matches the fully-qualified class name of your Kernel.', $class)); + } + + /** + * @var class-string $class + */ + $env = $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test'; + $debug = $_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true; + + return new $class($env, $debug); + } +} diff --git a/src/Test/PHPUnit/ShutdownKernelOnTestSuiteLoaded.php b/src/Test/PHPUnit/ShutdownKernelOnTestSuiteLoaded.php new file mode 100644 index 000000000..21eddd34a --- /dev/null +++ b/src/Test/PHPUnit/ShutdownKernelOnTestSuiteLoaded.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Test\PHPUnit; + +use PHPUnit\Event; +use Symfony\Component\HttpKernel\KernelInterface; +use Zenstruck\Foundry\Configuration; + +/** + * @author Nicolas PHILIPPE + */ +final class ShutdownKernelOnTestSuiteLoaded implements Event\TestSuite\LoadedSubscriber +{ + public function __construct( + private KernelInterface $kernel, + ) { + } + + public function notify(Event\TestSuite\Loaded $event): void + { + $this->kernel->shutdown(); + Configuration::shutdown(); + } +} diff --git a/tests/Fixture/SomeEnum.php b/tests/Fixture/SomeEnum.php index f91d224c6..a874a6fbc 100644 --- a/tests/Fixture/SomeEnum.php +++ b/tests/Fixture/SomeEnum.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture; enum SomeEnum diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..db38894dc --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -0,0 +1,60 @@ + + * + * 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\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + + /** + * @test + */ + #[Test] + #[DataProvider('createObjectFromServiceFactoryInDataProvider')] + public function it_can_create_one_object_in_data_provider(?Object1 $providedData): void + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + self::assertInstanceOf(Object1::class, $providedData); + $this->assertSame('router-constructor', $providedData->getProp1()); + } + + public static function createObjectFromServiceFactoryInDataProvider(): iterable + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null]; + + return; + } + + yield 'service factory' => [ + Object1Factory::createOne(), + ]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php new file mode 100644 index 000000000..c6b4a8285 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +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; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object2Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; +use Zenstruck\Foundry\Tests\Fixture\Object2; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +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 + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + self::assertEquals($expectedData, $providedData); + } + + public static function createObjectWithObjectFactoryInDataProvider(): iterable + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null, null]; + + return; + } + + yield 'object factory' => [Object2Factory::createOne(['object' => new Object1('prop1')]), new Object2(new Object1('prop1'))]; + yield 'service factory can be used if dependency is optional' => [Object1Factory::createOne(), new Object1('value1')]; + } + + #[Test] + #[DataProvider('createObjectWithPersistentObjectFactoryInDataProvider')] + public function assert_it_can_create_object_with_persistent_factory_in_data_provider(mixed $providedData, mixed $expectedData): void + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + self::assertEquals($expectedData, unproxy($providedData)); + } + + public static function createObjectWithPersistentObjectFactoryInDataProvider(): iterable + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null, null]; + + return; + } + + yield 'persistent factory' => [GenericEntityFactory::createOne(), new GenericEntity('default1')]; + yield 'proxy persistent factory' => [GenericProxyEntityFactory::createOne(), new GenericEntity('default1')]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..ec3de7b73 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +final class DataProviderWithNonProxyFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('throwsExceptionWhenCreatingObjectInDataProvider')] + public function it_throws_an_exception_when_trying_to_create_an_object_in_data_provider(?\Throwable $e): void + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + 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 + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null]; + + return; + } + + try { + GenericEntityFactory::createOne(); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php new file mode 100644 index 000000000..be2474041 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php @@ -0,0 +1,113 @@ + + * + * 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\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +abstract class DataProviderWithProxyFactoryInKernelTestCase extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('createOneObjectInDataProvider')] + public function assert_it_can_create_one_object_in_data_provider(?GenericModel $providedData): void + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + static::factory()::assert()->count(1); + + self::assertInstanceOf(Proxy::class, $providedData); + self::assertNotInstanceOf(Proxy::class, unproxy($providedData)); // asserts two proxies are not nested + self::assertInstanceOf(GenericModel::class, $providedData); + self::assertSame('value set in data provider', $providedData->getProp1()); + } + + public static function createOneObjectInDataProvider(): iterable + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null]; + + return; + } + + yield 'createOne()' => [ + static::factory()::createOne(['prop1' => 'value set in data provider']), + ]; + + yield 'create()' => [ + static::factory()->create(['prop1' => 'value set in data provider']), + ]; + } + + #[DataProvider('createMultipleObjectsInDataProvider')] + #[Test] + #[RequiresPhpunit('11.4')] + public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + { + if (\FOUNDRY_SKIP_DATA_PROVIDER === $this->dataName()) { + $this->markTestSkipped(); + } + + 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 + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + yield \FOUNDRY_SKIP_DATA_PROVIDER => [null]; + + return; + } + + yield 'createSequence()' => [ + static::factory()::createSequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ]), + ]; + + yield 'FactoryCollection::create()' => [ + static::factory()->sequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ])->create(), + ]; + } + + /** + * @return PersistentProxyObjectFactory + */ + abstract protected static function factory(): PersistentProxyObjectFactory; +} diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php new file mode 100644 index 000000000..0e636ceeb --- /dev/null +++ b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php @@ -0,0 +1,31 @@ + + * + * 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\RequiresPhpunit; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericProxyDocumentFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresMongo; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +final class GenericDocumentProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresMongo; + + protected static function factory(): GenericProxyDocumentFactory + { + return GenericProxyDocumentFactory::new(); + } +} diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php new file mode 100644 index 000000000..102a6418b --- /dev/null +++ b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php @@ -0,0 +1,31 @@ + + * + * 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\RequiresPhpunit; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +final class GenericEntityProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresORM; + + protected static function factory(): GenericProxyEntityFactory + { + return GenericProxyEntityFactory::new(); + } +} diff --git a/tests/Integration/Mongo/GenericDocumentFactoryTest.php b/tests/Integration/Mongo/GenericDocumentFactoryTest.php index ab6f5a606..df7be982f 100644 --- a/tests/Integration/Mongo/GenericDocumentFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\Mongo; use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresMongo; @@ -23,7 +22,7 @@ final class GenericDocumentFactoryTest extends GenericFactoryTestCase { use RequiresMongo; - protected function factory(): GenericModelFactory + protected static function factory(): GenericDocumentFactory { return GenericDocumentFactory::new(); } diff --git a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php index a3c581e21..6b41ce319 100644 --- a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericDocumentProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresMongo; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyDocumentFactory { return GenericProxyDocumentFactory::new(); } diff --git a/tests/Integration/ORM/GenericEntityFactoryTest.php b/tests/Integration/ORM/GenericEntityFactoryTest.php index 9b7458758..198e279ba 100644 --- a/tests/Integration/ORM/GenericEntityFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -23,7 +22,7 @@ final class GenericEntityFactoryTest extends GenericFactoryTestCase { use RequiresORM; - protected function factory(): GenericModelFactory + protected static function factory(): GenericEntityFactory { return GenericEntityFactory::new(); } diff --git a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php index 218c7b518..2542068fa 100644 --- a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericEntityProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresORM; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyEntityFactory { return GenericProxyEntityFactory::new(); } diff --git a/tests/Integration/Persistence/GenericFactoryTestCase.php b/tests/Integration/Persistence/GenericFactoryTestCase.php index ec3071bc4..7adf3dc9b 100644 --- a/tests/Integration/Persistence/GenericFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericFactoryTestCase.php @@ -43,27 +43,27 @@ abstract class GenericFactoryTestCase extends KernelTestCase */ public function can_create_and_update(): void { - $this->factory()::assert()->empty(); + static::factory()::assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()::assert() + static::factory()::assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); save($object); $this->assertSame('new value', $object->getProp1()); - $this->factory()::assert()->exists(['prop1' => 'new value']); + static::factory()::assert()->exists(['prop1' => 'new value']); } /** @@ -71,18 +71,18 @@ public function can_create_and_update(): void */ public function can_disable_auto_persist(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create(); + $object = static::factory()->withoutPersisting()->create(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); save($object); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -90,16 +90,16 @@ public function can_disable_auto_persist(): void */ public function can_refresh(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()->first(); + $ext = static::factory()->first(); $ext->setProp1('external'); save($ext); @@ -109,7 +109,7 @@ public function can_refresh(): void $this->assertSame($refreshed, $object); $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -117,18 +117,18 @@ public function can_refresh(): void */ public function cannot_refresh_if_there_are_unsaved_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { refresh($object); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); return; } @@ -141,13 +141,13 @@ public function cannot_refresh_if_there_are_unsaved_changes(): void */ public function can_delete(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); delete($object); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -170,9 +170,9 @@ public function repository_and_create_function(): void */ public function create_many(): void { - $models = $this->factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); + $models = static::factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); - $this->factory()::repository()->assert()->count(3); + static::factory()::repository()->assert()->count(3); $this->assertSame('value1', $models[0]->getProp1()); $this->assertSame('value2', $models[1]->getProp1()); @@ -184,10 +184,10 @@ public function create_many(): void */ public function find(): void { - $object = $this->factory()->create(['prop1' => 'foo']); + $object = static::factory()->create(['prop1' => 'foo']); - $this->assertSame($object->id, $this->factory()::find($object->id)->id); - $this->assertSame($object->id, $this->factory()::find(['prop1' => 'foo'])->id); + $this->assertSame($object->id, static::factory()::find($object->id)->id); + $this->assertSame($object->id, static::factory()::find(['prop1' => 'foo'])->id); } /** @@ -197,7 +197,7 @@ public function find_must_return_object(): void { $this->expectException(\RuntimeException::class); - $this->factory()::find(1); + static::factory()::find(1); } /** @@ -205,12 +205,12 @@ public function find_must_return_object(): void */ public function find_by(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $this->assertCount(1, $this->factory()::findBy(['prop1' => 'a'])); - $this->assertCount(2, $this->factory()::findBy(['prop1' => 'b'])); + $this->assertCount(1, static::factory()::findBy(['prop1' => 'a'])); + $this->assertCount(2, static::factory()::findBy(['prop1' => 'b'])); } /** @@ -218,15 +218,15 @@ public function find_by(): void */ public function find_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::findOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::findOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::findOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::findOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -234,12 +234,12 @@ public function find_or_create(): void */ public function random(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'c']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'c']); - $this->assertContains($this->factory()::random()->getProp1(), ['a', 'b', 'c']); - $this->assertSame('b', $this->factory()::random(['prop1' => 'b'])->getProp1()); + $this->assertContains(static::factory()::random()->getProp1(), ['a', 'b', 'c']); + $this->assertSame('b', static::factory()::random(['prop1' => 'b'])->getProp1()); } /** @@ -249,7 +249,7 @@ public function random_must_return_an_object(): void { $this->expectException(NotEnoughObjects::class); - $this->factory()::random(); + static::factory()::random(); } /** @@ -257,16 +257,16 @@ public function random_must_return_an_object(): void */ public function random_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::randomOrCreate()->getProp1()); - $this->assertSame('a', $this->factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate()->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -274,17 +274,17 @@ public function random_or_create(): void */ public function random_set(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $set = $this->factory()::randomSet(2); + $set = static::factory()::randomSet(2); $this->assertCount(2, $set); $this->assertContains($set[0]->getProp1(), ['a', 'b']); $this->assertContains($set[1]->getProp1(), ['a', 'b']); - $set = $this->factory()::randomSet(2, ['prop1' => 'b']); + $set = static::factory()::randomSet(2, ['prop1' => 'b']); $this->assertCount(2, $set); $this->assertSame('b', $set[0]->getProp1()); @@ -296,11 +296,11 @@ public function random_set(): void */ public function random_set_requires_at_least_the_number_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomSet(4); + static::factory()::randomSet(4); } /** @@ -308,12 +308,12 @@ public function random_set_requires_at_least_the_number_available(): void */ public function random_range(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $range = $this->factory()::randomRange(0, 3); + $range = static::factory()::randomRange(0, 3); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -322,7 +322,7 @@ public function random_range(): void $this->assertContains($object->getProp1(), ['a', 'b']); } - $range = $this->factory()::randomRange(0, 3, ['prop1' => 'b']); + $range = static::factory()::randomRange(0, 3, ['prop1' => 'b']); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -337,11 +337,11 @@ public function random_range(): void */ public function random_range_requires_at_least_the_max_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomRange(1, 5); + static::factory()::randomRange(1, 5); } /** @@ -349,12 +349,12 @@ public function random_range_requires_at_least_the_max_available(): void */ public function factory_count(): void { - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); - $this->assertSame(3, $this->factory()::count()); - $this->assertSame(2, $this->factory()::count(['prop1' => 'b'])); + $this->assertSame(3, static::factory()::count()); + $this->assertSame(2, static::factory()::count(['prop1' => 'b'])); } /** @@ -362,12 +362,12 @@ public function factory_count(): void */ public function truncate(): void { - $this->factory()::createMany(3); - $this->factory()::repository()->assert()->count(3); + static::factory()::createMany(3); + static::factory()::repository()->assert()->count(3); - $this->factory()::truncate(); + static::factory()::truncate(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); } /** @@ -375,9 +375,9 @@ public function truncate(): void */ public function factory_all(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); - $this->assertCount(3, $this->factory()::all()); + $this->assertCount(3, static::factory()::all()); } /** @@ -385,14 +385,14 @@ public function factory_all(): void */ public function repository_assertions(): void { - $assert = $this->factory()::repository()->assert(); + $assert = static::factory()::repository()->assert(); $assert->empty(); $assert->empty(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); $assert->notEmpty(); $assert->notEmpty(['prop1' => 'a']); @@ -415,9 +415,9 @@ public function repository_assertions(): void */ public function repository_is_lazy(): void { - $this->factory()::createOne(); + static::factory()::createOne(); - $repository = $this->factory()::repository(); + $repository = static::factory()::repository(); $object = $repository->random(); $object->setProp1('new value'); @@ -433,18 +433,18 @@ public function repository_is_lazy(): void */ public function flush_after(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); flush_after(function() { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); // ensure auto-refresh does not break when in flush_after $object->getProp1(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); }); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); } /** @@ -452,19 +452,19 @@ public function flush_after(): void */ public function can_disable_and_enable_persisting_globally(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); disable_persisting(); - $this->factory()::createOne(); - $this->factory()::new()->create(); + static::factory()::createOne(); + static::factory()::new()->create(); persistent_factory($this->modelClass())->create(['prop1' => 'foo']); persist($this->modelClass(), ['prop1' => 'foo']); enable_persisting(); - $this->factory()::createOne(); - $this->factory()::repository()->assert()->count(1); + static::factory()::createOne(); + static::factory()::repository()->assert()->count(1); } /** @@ -476,19 +476,19 @@ public function cannot_access_repository_method_when_persist_disabled(): void $countErrors = 0; try { - $this->factory()::assert(); + static::factory()::assert(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::repository(); + static::factory()::repository(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::findBy([]); + static::factory()::findBy([]); } catch (PersistenceDisabled) { ++$countErrors; } @@ -501,11 +501,11 @@ public function cannot_access_repository_method_when_persist_disabled(): void */ public function can_persist_object_with_sequence(): void { - $this->factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); + static::factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); - $this->factory()::assert()->count(2); - $this->factory()::assert()->exists(['prop1' => 'foo']); - $this->factory()::assert()->exists(['prop1' => 'bar']); + static::factory()::assert()->count(2); + static::factory()::assert()->exists(['prop1' => 'foo']); + static::factory()::assert()->exists(['prop1' => 'bar']); } /** @@ -519,7 +519,7 @@ public function assert_persist_is_re_enabled_automatically(): void self::assertTrue($configuration->persistence()->isEnabled()); persist($this->modelClass(), ['prop1' => 'value']); - $this->factory()::assert()->count(1); + static::factory()::assert()->count(1); } /** @@ -527,7 +527,7 @@ public function assert_persist_is_re_enabled_automatically(): void */ public function assert_it_ca_create_object_with_dates(): void { - $object = $this->factory()->create(['date' => $date = new \DateTimeImmutable()]); + $object = static::factory()->create(['date' => $date = new \DateTimeImmutable()]); self::assertSame($date->format(\DateTimeInterface::ATOM), $object->getDate()?->format(\DateTimeInterface::ATOM)); } @@ -545,11 +545,11 @@ public function it_should_not_create_proxy_for_not_persistable_objects(): void */ protected function modelClass(): string { - return $this->factory()::class(); + return static::factory()::class(); } /** * @return PersistentObjectFactory */ - abstract protected function factory(): PersistentObjectFactory; + abstract protected static function factory(): PersistentObjectFactory; } diff --git a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index e95976388..c8af22efb 100644 --- a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php @@ -11,6 +11,7 @@ namespace Zenstruck\Foundry\Tests\Integration\Persistence; +use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -18,6 +19,7 @@ use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\EntityWithReadonly\EntityWithReadonly; use Zenstruck\Foundry\Tests\Fixture\Model\Embeddable; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + use function Zenstruck\Foundry\factory; /** @@ -30,32 +32,32 @@ abstract class GenericProxyFactoryTestCase extends GenericFactoryTestCase */ public function can_update_and_delete_via_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); $this->assertSame('default1', $object->_refresh()->getProp1()); - $this->factory()->repository()->assert() + static::factory()->repository()->assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); $object->_save(); $this->assertSame('new value', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new value']); + static::factory()->repository()->assert()->exists(['prop1' => 'new value']); $object->_delete(); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -63,18 +65,18 @@ public function can_update_and_delete_via_proxy(): void */ public function can_disable_persisting_by_factory_and_save_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create()->_disableAutoRefresh(); + $object = static::factory()->withoutPersisting()->create()->_disableAutoRefresh(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); $object->_save(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -82,11 +84,11 @@ public function can_disable_persisting_by_factory_and_save_proxy(): void */ public function can_disable_and_enable_proxy_auto_refreshing(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_disableAutoRefresh(); $object->setProp1('new'); @@ -95,7 +97,7 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void $object->_save(); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -103,11 +105,11 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void */ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_withoutAutoRefresh(function(GenericModel&Proxy $object) { $object->setProp1('new'); @@ -116,7 +118,7 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo }); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -124,16 +126,16 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo */ public function can_manually_refresh_via_proxy(): void { - $object = $this->factory()->create()->_disableAutoRefresh(); + $object = static::factory()->create()->_disableAutoRefresh(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -143,7 +145,7 @@ public function can_manually_refresh_via_proxy(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -151,16 +153,16 @@ public function can_manually_refresh_via_proxy(): void */ public function proxy_auto_refreshes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -168,7 +170,7 @@ public function proxy_auto_refreshes(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -176,21 +178,21 @@ public function proxy_auto_refreshes(): void */ public function cannot_auto_refresh_proxy_if_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { $object->setProp1('new 1'); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_save(); $this->assertSame('new', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new']); + static::factory()->repository()->assert()->exists(['prop1' => 'new']); return; } @@ -203,11 +205,11 @@ public function cannot_auto_refresh_proxy_if_changes(): void */ public function can_access_repository_from_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $object = $object->_repository()->findOneBy(['prop1' => 'default1']); - $this->assertInstanceOf($this->factory()::class(), $object); + $this->assertInstanceOf(static::factory()::class(), $object); } /** @@ -215,7 +217,7 @@ public function can_access_repository_from_proxy(): void */ public function can_force_set_and_get_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $this->assertSame('default1', $object->_get('prop1')); @@ -229,7 +231,7 @@ public function can_force_set_and_get_proxy(): void */ public function can_get_real_object_even_if_modified(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->setProp1('foo'); self::assertInstanceOf(GenericModel::class, $real = $object->_real()); @@ -246,7 +248,7 @@ public function can_create_object_with_readonly_properties(): void $objectWithReadOnly = $factory::createOne([ 'prop' => 1, 'embedded' => factory(Embeddable::class, ['prop1' => 'value1']), - 'date' => new \DateTimeImmutable() + 'date' => new \DateTimeImmutable(), ]); $objectWithReadOnly->_refresh(); @@ -259,7 +261,7 @@ public function can_create_object_with_readonly_properties(): void */ public function can_delete_proxified_object_and_still_access_its_methods(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->_delete(); $this->assertSame('default1', $object->getProp1()); @@ -270,9 +272,9 @@ public function can_delete_proxified_object_and_still_access_its_methods(): void */ public function can_use_after_persist_with_attributes(): void { - $object = $this->factory() + $object = static::factory() ->instantiateWith(Instantiator::withConstructor()->allowExtra('extra')) - ->afterPersist(function (GenericModel $object, array $attributes) { + ->afterPersist(function(GenericModel $object, array $attributes) { $object->setProp1($attributes['extra']); }) ->create(['extra' => $value = 'value set with after persist']); @@ -283,7 +285,7 @@ public function can_use_after_persist_with_attributes(): void /** * @return PersistentProxyObjectFactory */ - abstract protected function factory(): PersistentProxyObjectFactory; // @phpstan-ignore-line + abstract protected static function factory(): PersistentProxyObjectFactory; /** * @return PersistentProxyObjectFactory diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 51fb6d505..d2a607084 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,7 @@ use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\ORM\AbstractORMPersistenceStrategy; use Zenstruck\Foundry\Tests\Fixture\TestKernel; @@ -53,3 +54,9 @@ } \set_exception_handler([new ErrorHandler(), 'handleException']); + +/** + * Some tests with data providers are only meant to be tested with Foundry's extension for PHPUnit, + * The only way to skip them for now is to skip them based on a data provider name. + */ +const FOUNDRY_SKIP_DATA_PROVIDER = 'skip_data_provider';