Skip to content

Commit

Permalink
Merge pull request #125 from vierge-noire/cake3_next
Browse files Browse the repository at this point in the history
v1.4
  • Loading branch information
pabloelcolombiano authored Sep 8, 2021
2 parents 1368929 + 79ae379 commit 91a4c94
Show file tree
Hide file tree
Showing 31 changed files with 421 additions and 474 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/tests_composer2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ jobs:
php-version: ['7.1', '7.4']
db-type: [sqlite, mysql, pgsql]
composer-type: [lowest, stable, dev]
exclude:
# Mysql CI triggers error on 7.1
- db-type: mysql
php-version: '7.1'


name: PHP ${{ matrix.php-version }} & ${{ matrix.db-type }} & ${{ matrix.composer-type }}

Expand Down Expand Up @@ -64,4 +69,4 @@ jobs:
if [ ${{ matrix.db-type }} == 'mysql' ]; then
sudo service mysql start && mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE IF NOT EXISTS test_fixture_factories;';
fi
composer ${{ matrix.db-type }}
composer ${{ matrix.db-type }}
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ The `fixture_factories_persist` command is featured on CakePHP 4 only (open to c
You can create scenarios that will persist a multitude of test fixtures. Use the `CakephpFixtureFactories\Scenario\ScenarioAwareTrait`
in your test and load your scenario with the `loadFixtureScenario()` method. You can either provide the
fully qualified name of the scenario class, or place your scenarios under the `App\Test\Scenario` namespace.
Example:
```$xslt
$authors = $this->loadFixtureScenario('NAustralianAuthors', 3);
```
will persist 3 authors associated to the country Australia, as defined [in this example scenario](tests/Scenario/NAustralianAuthorsScenario.php).

Scenarios should implement the `CakephpFixtureFactories\Scenario\FixtureScenarioInterface` class.
[This test](tests/TestCase/Scenario/FixtureScenarioTest.php) provides an example on how to use scenarios.
Expand Down
162 changes: 116 additions & 46 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,122 @@
## Creating Test Fixtures

### Static fixtures
### Basic usage

Here are some examples of how to use the fixture factories.

One article with a random title, as defined in the factory [on the previous page](factories.md):
```
```php
$article = ArticleFactory::make()->getEntity();
```
Two articles with different random titles:
```
Two articles with different random titles:
```php
$articles = ArticleFactory::make(2)->getEntities();
```
One article with title set to 'Foo':
```
One article with title set to 'Foo':
```php
$article = ArticleFactory::make(['title' => 'Foo'])->getEntity();
```
Three articles with the title set to 'Foo':
```
Three articles with the title set to 'Foo':
```php
$articles = ArticleFactory::make(['title' => 'Foo'], 3)->getEntities();
```

or

```
or
```php
$articles = ArticleFactory::make(3)->patchData(['title' => 'Foo'])->getEntities();
```

or

```php
$articles = ArticleFactory::make(3)->setField('title', 'Foo')->getEntities();
```
or
```php
$articles = ArticleFactory::make()->setField('title', 'Foo')->setTimes(3)->getEntities();
```
$articles = ArticleFactory::make()->patchData(['title' => 'Foo'])->setTimes(3)->getEntities();
or
```php
$articles = ArticleFactory::make([
['title' => 'Foo'],
['title' => 'Bar'],
['title' => 'Baz'],
])->getEntities();
```

In order to persist the data generated, use the method `persist` instead of `getEntity` resp. `getEntities`:
```
```php
$articles = ArticleFactory::make(3)->persist();
```

### Dynamic fixtures
The drawback of the previous example, is that, if you haven't defined the `title` field with `faker` in the `setDefaultTemplate` method, all the generated examples have the same title. The following
generates three articles with different random titles:
```
### Using `FactoryAwareTrait`
All examples above are using static getter to fetch a factory instance. As convenience and kinda syntactic sugar, you can use the `FactoryAwareTrait::getFactory` instead.

`getFactory` is more tolerant on provided name, as you can use plurals or lowercased names. All arguments passed after factory name will be cast to `BaseFactory::make`.

```php
use App\Test\Factory\ArticleFactory;
use Faker\Generator;
...
$articles = ArticleFactory::make(function(ArticleFactory $factory, Generator $faker) {
return [
'title' => $faker->text,
];
}, 3)->persist();
use CakephpFixtureFactories\Factory\FactoryAwareTrait;

class MyTest extends TestCase
{
use FactoryAwareTrait;

public function myTest(): void
{
// Static getter style
$article = ArticleFactory::make()->getEntity();
$article = ArticleFactory::make(['title' => 'Foo'])->getEntity();
$articles = ArticleFactory::make(3)->getEntities();
$articles = ArticleFactory::make(['title' => 'Foo'], 3)->getEntities();

// Exactly the same in FactoryAwareTrait style
$article = $this->getFactory('Article')->getEntity();
$article = $this->getFactory('Article', ['title' => 'Foo'])->getEntity();
$articles = $this->getFactory('Article', 3)->getEntities();
$articles = $this->getFactory('Article', ['title' => 'Foo'], 3)->getEntities();
}
}
```

### Chaining methods
The aim of the test fixture factories is to bring business coherence in your test fixtures.
This can be simply achieved using the chainable methods of your factories. As long as those return `$this`, you may chain as much methods as you require.
In the following example, we make use of a method in the Article factory in order to easily create articles with a job title.
It is a simple study case, but this could be any pattern of your business logic.
```
It is a simple study case, but this could be any pattern of your business logic.
```php
$articleFactory = ArticleFactory::make(['title' => 'Foo']);
$articleFoo1 = $articleFactory->persist();
$articleFoo2 = $articleFactory->persist();
$articleJobOffer = $articleFactory->setJobTitle()->persist();
```

The two first articles have a title set two 'Foo'. The third one has a job title, which is randomly generated by fake, as defined in the
`ArticleFactory`.
### Populating associations: the _with_ method
If you have baked your factories with the option `-m` or `--methods`, you will have noticed that a method for each association
has been inserted in the factories. This will assist you creating fixtures for the associated models. For example, we can
create an article with 10 authors as follow:
```
`ArticleFactory`.

### Populating associations: the _with_ method
If you have baked your factories with the option `-m` or `--methods`, you will have noticed that a method for each association
has been inserted in the factories. This will assist you creating fixtures for the associated models. For example, we can
create an article with 10 authors as follows:
```php
use App\Test\Factory\ArticleFactory;
use App\Test\Factory\AuthorFactory;
use Faker\Generator;
...
$article = ArticleFactory::make()->with('Authors', AuthorFactory::make(10))->persist();
```
or using the method defined in our `ArticleFactory`:
```
```php
$article = ArticleFactory::make()->withAuthors(10)->persist();
```

If we wish to randomly populate the field `biography` of the 10 authors of our article, with 10 different biographies:
```
```php
$article = ArticleFactory::make()->withAuthors(function(AuthorFactory $factory, Generator $faker) {
return [
'biography' => $faker->realText()
];
}, 10)->persist();
```
It is also possible to use the _dot_ notation to create associated fixtures:
```
```php
$article = ArticleFactory::make()->with('Authors.Address.City.Country', ['name' => 'Kenya'])->persist();
```
will create an article, with an author having itself an address in Kenya.
Expand All @@ -103,21 +127,67 @@ The second parameter of the method with can be:
* a factory

Ultimately, the square bracket notation provides a mean to specify the number of associated
data create:
```
data created:
```php
$article = ArticleFactory::make(5)->with('Authors[3].Address.City.Country', ['name' => 'Kenya'])->persist();
```
will create 5 articles, having themselves each 3 different associated authors, all located in Kenya.

It is also possible to specify the fields of a toMany associated model.
For example, if we wish to create a random country with two cities having known names:

```
```php
$country = CountryFactory::make()->with('Cities', [
['name' => 'Nairobi'],
['name' => 'Mombasa'],
])->persist();
```

This an be useful if your business logic uses hard coded values, or constants.Note that when an association has the same name as a virtual field,
the virtual field will overwrite the data prepared by the associated factory.
This can be useful if your business logic uses hard coded values, or constants.

Note that when an association has the same name as a virtual field,
the virtual field will overwrite the data prepared by the associated factory.

### Factory injection

When building associations, you may simply provide a factory as parameter. Example:

```php
$country = CountryFactory::make()->with('Cities',
CityFactory::make()->threeCitiesAndFiveVillages()
)->persist();
```
will provide a country associated with three cities and five villages.

### Entity injection

You may also inject an exiting entity. The previous example would be now:
```php
$threeCitiesAndFiveVillages = CityFactory::make()->threeCitiesAndFiveVillages()->getEntities();
$country = CountryFactory::make()->with('Cities', $threeCitiesAndFiveVillages)->persist();
```

You may also pass an array of factories:
```php
$threeCitiesAndFiveVillages = CityFactory::make()->threeCitiesAndFiveVillages()->getEntities();
$country = CountryFactory::make()->with('Cities', [
CityFactory::make()->threeCitiesAndFiveVillages(),
CityFactory::make()->capitalCity()
])->persist();
```

### With a callable

In case a given field has not been specified with `faker` in the `setDefaultTemplate` method, all the generated fields of a given factory
will be identical. The following
generates three articles with different random titles:
```php
use App\Test\Factory\ArticleFactory;
use Faker\Generator;
...
$articles = ArticleFactory::make(function(ArticleFactory $factory, Generator $faker) {
return [
'title' => $faker->text,
];
}, 3)->persist();
```
4 changes: 1 addition & 3 deletions docs/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ class ArticleFactory extends BaseFactory
*/
public function setJobTitle()
{
return $this->patchData([
'title' => $this->getFaker()->jobTitle,
]);
return $this->setField('title', $this->getFaker()->jobTitle());
}
}
```
Expand Down
27 changes: 27 additions & 0 deletions src/Factory/BaseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,18 @@ public function patchData(array $data): self
return $this;
}

/**
* Sets the value for a single field
*
* @param string $field to set
* @param mixed $value to assign
* @return $this
*/
public function setField(string $field, $value)
{
return $this->patchData([$field => $value]);
}

/**
* A protected class dedicated to generating / collecting data for this factory
* @return DataCompiler
Expand Down Expand Up @@ -565,16 +577,31 @@ public function mergeAssociated(array $data): self
* @param string $type the type of query to perform
* @param array $options An array that will be passed to Query::applyOptions()
* @return \Cake\ORM\Query The query builder
* @see Query::find()
*/
public static function find(string $type = 'all', array $options = []): Query
{
return self::make()->getTable()->find($type, $options);
}

/**
* Get from primary key the factory's related table entries, without before find.
*
* @param mixed $primaryKey primary key value to find
* @param array $options options accepted by `Table::find()`
* @return \Cake\Datasource\EntityInterface
* @see Table::get()
*/
public static function get($primaryKey, array $options = []): EntityInterface
{
return self::make()->getTable()->get($primaryKey, $options);
}

/**
* Count the factory's related table entries without before find.
*
* @return int
* @see Query::count()
*/
public static function count(): int
{
Expand Down
13 changes: 10 additions & 3 deletions src/Factory/DataCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,16 @@ public function getCompiledTemplateData()
if (is_array($this->dataFromInstantiation) && isset($this->dataFromInstantiation[0])) {
$compiledTemplateData = [];
foreach ($this->dataFromInstantiation as $entity) {
$compiledTemplateData[] = $this->compileEntity($entity, $setPrimaryKey);
// Only the first entity gets its primary key set.
$setPrimaryKey = false;
if ($entity instanceof BaseFactory) {
foreach ($entity->getEntities() as $subEntity) {
$compiledTemplateData[] = $this->compileEntity($subEntity, $setPrimaryKey);
$setPrimaryKey = false;
}
} else {
$compiledTemplateData[] = $this->compileEntity($entity, $setPrimaryKey);
// Only the first entity gets its primary key set.
$setPrimaryKey = false;
}
}
} else {
$compiledTemplateData = $this->compileEntity($this->dataFromInstantiation, $setPrimaryKey);
Expand Down
5 changes: 3 additions & 2 deletions src/Scenario/FixtureScenarioInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ interface FixtureScenarioInterface
/**
* Create your bunch of test fixtures in this method.
*
* @return void
* @param mixed ...$args Arguments passed to the scenario.
* @return mixed
*/
public function load();
public function load(...$args);
}
Loading

0 comments on commit 91a4c94

Please sign in to comment.