Skip to content

Commit 5ec618e

Browse files
committed
refactor: ony run twice data providers using Foundry
1 parent c6f3ba7 commit 5ec618e

14 files changed

+289
-203
lines changed

src/Exception/FoundryNotBooted.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class FoundryNotBooted extends \LogicException
2020
{
2121
public function __construct()
2222
{
23-
$message = FoundryExtension::isEnabled()
23+
$message = FoundryExtension::shouldBeEnabled()
2424
? 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure Foundry\'s PHPUnit extension is enabled.'
2525
: 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.';
2626

src/FactoryCollection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ public static function range(Factory $factory, int $min, int $max): self
125125
throw new \InvalidArgumentException('Min must be less than max.');
126126
}
127127

128+
if ($factory instanceof PersistentObjectFactory && $factory->isPersisting() && Configuration::instance()->inADataProvider()) {
129+
throw new \InvalidArgumentException('Using randomized "range" factory in data provider is not supported.');
130+
}
131+
128132
return new self($factory, static fn() => \array_fill(0, \mt_rand($min, $max), []));
129133
}
130134

src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1919
use Zenstruck\Foundry\Configuration;
2020
use Zenstruck\Foundry\InMemory\AsInMemoryTest;
21-
use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
2221
use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
2322
use Zenstruck\Foundry\Test\UnitTestConfig;
2423

@@ -32,12 +31,6 @@ public function notify(Event\Test\DataProviderMethodCalled $event): void
3231
{
3332
$this->bootFoundryForDataProvider($event->testMethod()->className());
3433

35-
PersistentObjectFromDataProviderRegistry::instance()->addDataset(
36-
$event->testMethod()->className(),
37-
$event->testMethod()->methodName(),
38-
"{$event->dataProviderMethod()->className()}::{$event->dataProviderMethod()->methodName()}"(...) // @phpstan-ignore callable.nonCallable
39-
);
40-
4134
$testMethod = $event->testMethod();
4235

4336
if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) {

src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use PHPUnit\Event;
1717
use Zenstruck\Foundry\Configuration;
18+
use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
1819
use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
1920

2021
/**
@@ -25,6 +26,12 @@ final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\Da
2526
{
2627
public function notify(Event\Test\DataProviderMethodFinished $event): void
2728
{
29+
PersistentObjectFromDataProviderRegistry::instance()->storeDatasetIfFoundryWasUsedInDataProvider(
30+
$event->testMethod()->className(),
31+
$event->testMethod()->methodName(),
32+
...$event->calledMethods(),
33+
);
34+
2835
KernelTestCaseHelper::tearDownClass($event->testMethod()->className());
2936

3037
Configuration::shutdown();

src/Persistence/IsProxy.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Zenstruck\Foundry\Configuration;
1717
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
1818
use Zenstruck\Foundry\Object\Hydrator;
19+
use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
1920
use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist;
2021

2122
/**
@@ -152,7 +153,7 @@ private function _autoRefresh(): void
152153
// we don't want that "transparent" calls to _refresh() to trigger a PersistenceNotAvailable exception
153154
// or a RefreshObjectFailed exception when the object was deleted
154155
$this->_refresh();
155-
} catch (PersistenceNotAvailable|ObjectNoLongerExist) {
156+
} catch (PersistenceNotAvailable|ObjectNoLongerExist|NoPersistenceStrategy) {
156157
}
157158
}
158159

src/Persistence/PersistentObjectFromDataProviderRegistry.php

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Zenstruck\Foundry\Persistence;
1313

14+
use PHPUnit\Event\Code\ClassMethod;
15+
1416
/**
15-
* If a persistent object has been created in a data provider, we need to initialize the proxy object,
17+
* If a persistent object has been created in a data provider, we need to initialize the lazy object,
1618
* which will trigger the object to be persisted.
1719
*
18-
* Otherwise, such test would not pass:
20+
* Otherwise, such a test would not pass:
1921
* ```php
2022
* #[DataProvider('provide')]
2123
* public function testSomething(MyEntity $entity): void
@@ -31,9 +33,12 @@
3133
*
3234
* Sadly, this cannot be done directly a subscriber, since PHPUnit does not give access to the actual tests instances.
3335
*
34-
* This class is highly hacky!
35-
* We collect all the "datasets" and we trigger the persistence for each one before the test is executed.
36-
* This means that de data providers are called twice.
36+
* ⚠️ This class is highly hacky!
37+
*
38+
* If we detect that a persisting object was created in a data provider, we collect the "datasets" of the test,
39+
* and we trigger the persistence of these objects before the test is executed.
40+
*
41+
* This means that the data providers using Foundry are called twice.
3742
* To prevent the persisted object from being different from the one returned by the data provider, we use a "buffer" so
3843
* that we can return the same object for each data provider call.
3944
*
@@ -49,30 +54,39 @@ final class PersistentObjectFromDataProviderRegistry
4954
/** @var list<object> */
5055
private array $objectsBuffer = [];
5156

52-
private bool $shouldReturnExistingObject = false;
57+
private bool $shouldReturnObjectFromBuffer = false;
5358

5459
public static function instance(): self
5560
{
5661
return self::$instance ?? self::$instance = new self();
5762
}
5863

59-
/**
60-
* @param callable():iterable<array-key, mixed> $dataProviderResult
61-
*/
62-
public function addDataset(string $className, string $methodName, callable $dataProviderResult): void
64+
public function storeDatasetIfFoundryWasUsedInDataProvider(string $className, string $methodName, ClassMethod ...$calledMethods): void
6365
{
64-
$this->shouldReturnExistingObject = false;
66+
if (count($this->objectsBuffer) === 0) {
67+
return;
68+
}
69+
70+
$this->shouldReturnObjectFromBuffer = true;
71+
72+
$testCaseContext = $this->testCaseContext($className, $methodName);
73+
$this->datasets[$testCaseContext] = [];
74+
75+
foreach ($calledMethods as $calledMethod) {
76+
$dataProviderResult = "{$calledMethod->className()}::{$calledMethod->methodName()}"(); // @phpstan-ignore callable.nonCallable
6577

66-
$dataProviderResult = $dataProviderResult();
78+
if (!\is_array($dataProviderResult)) {
79+
$dataProviderResult = \iterator_to_array($dataProviderResult);
80+
}
6781

68-
if (!\is_array($dataProviderResult)) {
69-
$dataProviderResult = \iterator_to_array($dataProviderResult);
82+
$this->datasets[$testCaseContext] = [...$this->datasets[$testCaseContext], ...$dataProviderResult];
7083
}
7184

72-
$testCaseContext = $this->testCaseContext($className, $methodName);
73-
$this->datasets[$testCaseContext] = $dataProviderResult;
85+
$this->shouldReturnObjectFromBuffer = false;
7486

75-
$this->shouldReturnExistingObject = true;
87+
if (count($this->objectsBuffer) !== 0) { // @phpstan-ignore notIdentical.alwaysTrue
88+
throw new \InvalidArgumentException("No object found. Hint: make sure you're not creating a randomized number of objects with Foundry in a data provider, as they are not supported.");
89+
}
7690
}
7791

7892
/**
@@ -84,10 +98,18 @@ public function addDataset(string $className, string $methodName, callable $data
8498
*/
8599
public function deferObjectCreation(PersistentObjectFactory $factory): object
86100
{
87-
if (!$this->shouldReturnExistingObject) {
101+
if (!$factory->isPersisting()) {
102+
return $factory->create();
103+
}
104+
105+
if (!$this->shouldReturnObjectFromBuffer) {
88106
return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory);
89107
}
90108

109+
if (count($this->objectsBuffer) === 0) {
110+
throw new \InvalidArgumentException("No object found. Hint: make sure you're not creating a randomized number of objects with Foundry in a data provider, as they are not supported.");
111+
}
112+
91113
return \array_shift($this->objectsBuffer); // @phpstan-ignore return.type
92114
}
93115

@@ -99,7 +121,7 @@ public function triggerPersistenceForDataset(string $className, string $methodNa
99121
return;
100122
}
101123

102-
initialize_proxy_object($this->datasets[$testCaseContext][$dataSetName]);
124+
initialize_lazy_object($this->datasets[$testCaseContext][$dataSetName]);
103125

104126
unset($this->datasets[$testCaseContext][$dataSetName]);
105127
}

src/Persistence/functions.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function assert_not_persisted(object $object, string $message = '{entity} is per
230230
/**
231231
* @internal
232232
*/
233-
function initialize_proxy_object(mixed $what): void
233+
function initialize_lazy_object(mixed $what): void
234234
{
235235
if (
236236
\PHP_VERSION_ID >= 80400
@@ -244,7 +244,7 @@ function initialize_proxy_object(mixed $what): void
244244

245245
match (true) {
246246
$what instanceof Proxy => $what->_initializeLazyObject(),
247-
\is_array($what) => \array_map(initialize_proxy_object(...), $what),
247+
\is_array($what) => \array_map(initialize_lazy_object(...), $what),
248248
default => true, // do nothing
249249
};
250250
}

src/Test/Factories.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Zenstruck\Foundry\Configuration;
1818
use Zenstruck\Foundry\PHPUnit\FoundryExtension;
1919

20-
use function Zenstruck\Foundry\Persistence\initialize_proxy_object;
20+
use function Zenstruck\Foundry\Persistence\initialize_lazy_object;
2121

2222
/**
2323
* @author Kevin Bond <kevinbond@gmail.com>
@@ -109,6 +109,6 @@ private function _loadDataProvidedProxies(): void
109109
? $this->getProvidedData() // @phpstan-ignore method.notFound
110110
: $this->providedData(); // @phpstan-ignore method.internal
111111

112-
initialize_proxy_object($providedData);
112+
initialize_lazy_object($providedData);
113113
}
114114
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <kevinbond@gmail.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Tests\Integration\DataProvider;
15+
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\Attributes\RequiresEnvironmentVariable;
18+
use PHPUnit\Framework\Attributes\RequiresPhp;
19+
use PHPUnit\Framework\Attributes\RequiresPhpunit;
20+
use PHPUnit\Framework\Attributes\RequiresPhpunitExtension;
21+
use PHPUnit\Framework\Attributes\Test;
22+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
23+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
24+
use Zenstruck\Foundry\PHPUnit\FoundryExtension;
25+
use Zenstruck\Foundry\Test\ResetDatabase;
26+
use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory;
27+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
28+
use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel;
29+
use Zenstruck\Foundry\Tests\Integration\RequiresMongo;
30+
31+
/**
32+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
33+
* @requires PHPUnit >=12
34+
*/
35+
#[RequiresPhpunit('>=12')]
36+
#[RequiresPhp('>=8.4')]
37+
#[RequiresPhpunitExtension(FoundryExtension::class)]
38+
#[RequiresEnvironmentVariable('USE_PHP_84_LAZY_OBJECTS', '1')]
39+
final class DataProviderWithPersistentDocumentFactoryTest extends DataProviderWithPersistentFactoryTestCase
40+
{
41+
use RequiresMongo;
42+
43+
protected static function factory(): PersistentObjectFactory
44+
{
45+
return GenericDocumentFactory::new();
46+
}
47+
}
Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
use PHPUnit\Framework\Attributes\RequiresPhpunitExtension;
2121
use PHPUnit\Framework\Attributes\Test;
2222
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
23+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
2324
use Zenstruck\Foundry\PHPUnit\FoundryExtension;
2425
use Zenstruck\Foundry\Test\ResetDatabase;
2526
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
2627
use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel;
28+
use Zenstruck\Foundry\Tests\Integration\RequiresORM;
2729

2830
/**
2931
* @author Nicolas PHILIPPE <nikophil@gmail.com>
@@ -33,9 +35,9 @@
3335
#[RequiresPhp('>=8.4')]
3436
#[RequiresPhpunitExtension(FoundryExtension::class)]
3537
#[RequiresEnvironmentVariable('USE_PHP_84_LAZY_OBJECTS', '1')]
36-
final class DataProviderWithPersistentFactoryAndPHP84InKernelTest extends KernelTestCase
38+
final class DataProviderWithPersistentEntityFactoryTest extends DataProviderWithPersistentFactoryTestCase
3739
{
38-
use ResetDatabase;
40+
use RequiresORM;
3941

4042
#[Test]
4143
#[DataProvider('createOneObjectInDataProvider')]
@@ -45,49 +47,17 @@ public function assert_it_can_create_one_object_in_data_provider(?GenericModel $
4547

4648
self::assertNotNull($providedData);
4749
self::assertFalse((new \ReflectionClass($providedData))->isUninitializedLazyObject($providedData));
48-
self::assertSame('value set in data provider', $providedData->getProp1());
4950
}
5051

5152
public static function createOneObjectInDataProvider(): iterable
5253
{
5354
yield 'createOne()' => [
5455
GenericEntityFactory::createOne(['prop1' => 'value set in data provider']),
5556
];
56-
57-
yield 'create()' => [
58-
GenericEntityFactory::new()->create(['prop1' => 'value set in data provider']),
59-
];
6057
}
6158

62-
#[Test]
63-
#[DataProvider('createMultipleObjectsInDataProvider')]
64-
public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void
59+
protected static function factory(): PersistentObjectFactory
6560
{
66-
self::assertIsArray($providedData);
67-
GenericEntityFactory::assert()->count(2);
68-
69-
foreach ($providedData as $providedDatum) {
70-
self::assertFalse((new \ReflectionClass($providedDatum))->isUninitializedLazyObject($providedDatum));
71-
}
72-
73-
self::assertSame('prop 1', $providedData[0]->getProp1());
74-
self::assertSame('prop 2', $providedData[1]->getProp1());
75-
}
76-
77-
public static function createMultipleObjectsInDataProvider(): iterable
78-
{
79-
yield 'createSequence()' => [
80-
GenericEntityFactory::createSequence([
81-
['prop1' => 'prop 1'],
82-
['prop1' => 'prop 2'],
83-
]),
84-
];
85-
86-
yield 'FactoryCollection::create()' => [
87-
GenericEntityFactory::new()->sequence([
88-
['prop1' => 'prop 1'],
89-
['prop1' => 'prop 2'],
90-
])->create(),
91-
];
61+
return GenericEntityFactory::new();
9262
}
9363
}

0 commit comments

Comments
 (0)