Skip to content

Commit

Permalink
Merge pull request #66 from shaffe-fr/feat/immutable
Browse files Browse the repository at this point in the history
Make factories immutable by default
  • Loading branch information
christophrumpel committed Feb 22, 2021
2 parents 1b927ee + a69d318 commit 3cc0fa0
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 4 deletions.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ $factory->state(User::class, 'active', function () {
While creating a new class factory, you will be asked if you like those states to be imported to your new factories. If you agree, you can immediately use them. The state `active` is now a method on your `UserFactory`.

```php
$recipe = UserFactory::new()
$user = UserFactory::new()
->active()
->create();
```
Expand Down Expand Up @@ -230,6 +230,40 @@ public function active(): UserFactory

This is recommended for all methods which you will use to setup your test model. If you wouldn't clone the factory, you will always modify the factory itself. This could lead into problems when you use the same factory again.

To make a whole factory immutable by default, set the `$immutable` property to `true`. That way, every state change will automatically return a cloned instance.

```php
class UserFactory
{
protected string $modelClass = User::class;
protected bool $immutable = true;

// ...

public function active(): UserFactory
{
return $this->overwriteDefaults([
'active' => true,
]);
}
}
```

In some context, you might want to use a standard factory as immutable. This can be done with the `immutable` method.

```php
$factory = UserFactory::new()
->immutable();

$activeUser = $factory
->active()
->create();

$inactiveUser = $factory->create();
```

> **Note**: `with` and `withFactory` methods are always immutable.
### What Else

The best thing about those new factory classes is that you `own` them. You can create as many methods or properties as you like to help you create those specific instances that you need. Here is how a more complex factory call could look like:
Expand All @@ -241,7 +275,7 @@ UserFactory::new()
->withRecipesAndIngredients()
->times(10)
->create();
```
```

Using such a factory call will help your tests to stay clean and give everyone a good overview of what is happening here.

Expand Down
46 changes: 46 additions & 0 deletions example/tests/Factories/RecipeFactoryImmutable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace ExampleAppTests\Factories;

use App\Models\Recipe;
use Christophrumpel\LaravelFactoriesReloaded\BaseFactory;
use Faker\Generator;

class RecipeFactoryImmutable extends BaseFactory
{
protected string $modelClass = Recipe::class;

protected bool $immutable = true;

public function create(array $extra = []): Recipe
{
return $this->build($extra);
}

public function make(array $extra = []): Recipe
{
return $this->build($extra, 'make');
}

public function getDefaults(Generator $faker): array
{
return [
'name' => 'Lasagne',
'description' => 'Our family lasagne recipe.',
];
}

public function pasta(): self
{
return $this->overwriteDefaults([
'name' => 'Pasta',
]);
}

public function pizza(): self
{
return $this->overwriteDefaults([
'name' => 'Pizza',
]);
}
}
16 changes: 14 additions & 2 deletions src/BaseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ abstract class BaseFactory implements FactoryInterface

protected string $modelClass;

protected bool $immutable = false;

protected Collection $relatedModelFactories;

protected Generator $faker;
Expand All @@ -34,6 +36,14 @@ public static function new(): self
return new static($faker);
}

/** @return static */
public function immutable(bool $immutable = true): self
{
$this->immutable = $immutable;

return $this->immutable ? clone $this : $this;
}

protected function build(array $extra = [], string $creationType = 'create')
{
$modelData = $this->transformModelFields(
Expand Down Expand Up @@ -91,13 +101,15 @@ public function withFactory(FactoryInterface $relatedFactory, string $relationsh
*/
public function overwriteDefaults($attributes): self
{
$clone = $this->immutable ? clone $this : $this;

if (is_callable($attributes)) {
$attributes = $attributes();
}

$this->overwriteDefaults = array_merge($this->overwriteDefaults, $attributes);
$clone->overwriteDefaults = array_merge($clone->overwriteDefaults, $attributes);

return $this;
return $clone;
}

protected function getFactoryFromClassName(string $className): FactoryInterface
Expand Down
34 changes: 34 additions & 0 deletions tests/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use ExampleAppTests\Factories\GroupFactoryUsingFaker;
use ExampleAppTests\Factories\IngredientFactoryUsingClosure;
use ExampleAppTests\Factories\RecipeFactory;
use ExampleAppTests\Factories\RecipeFactoryImmutable;
use ExampleAppTests\Factories\RecipeFactoryUsingFactoryForRelationship;
use ExampleAppTests\Factories\RecipeFactoryUsingLaravelFactoryForRelationship;
use Illuminate\Foundation\Testing\RefreshDatabase;
Expand Down Expand Up @@ -150,6 +151,39 @@ public function it_lets_you_overwrite_default_data_when_making_multiple_instance
$this->assertEquals('Pancakes', $pancakes->first()->name);
}

/** @test */
public function it_lets_you_create_immutable_factories_by_default(): void
{
$recipe = RecipeFactoryImmutable::new()->overwriteDefaults(['name' => 'Pasta']);
$firstRecipe = $recipe->overwriteDefaults(['name' => 'Pizza'])->create();
$secondRecipe = $recipe->create();

$this->assertEquals('Pizza', $firstRecipe->name);
$this->assertEquals('Pasta', $secondRecipe->name);
}

/** @test */
public function it_makes_immutable_factory_methods_immutable(): void
{
$recipe = RecipeFactoryImmutable::new()->pasta();
$firstRecipe = $recipe->pizza()->create();
$secondRecipe = $recipe->create();

$this->assertEquals('Pizza', $firstRecipe->name);
$this->assertEquals('Pasta', $secondRecipe->name);
}

/** @test */
public function it_lets_you_use_a_mutable_factory_as_immutable(): void
{
$recipe = RecipeFactory::new()->immutable()->overwriteDefaults(['name' => 'Pasta']);
$firstRecipe = $recipe->overwriteDefaults(['name' => 'Pizza'])->create();
$secondRecipe = $recipe->create();

$this->assertEquals('Pizza', $firstRecipe->name);
$this->assertEquals('Pasta', $secondRecipe->name);
}

/** @test */
public function it_does_not_fill_field_that_is_not_fillable_according_to_config(): void
{
Expand Down

0 comments on commit 3cc0fa0

Please sign in to comment.