From 4e27e2ee4039bed0c8afee2b59b29b169b9d9e16 Mon Sep 17 00:00:00 2001 From: Karel FAILLE Date: Mon, 15 Feb 2021 15:30:06 +0100 Subject: [PATCH 1/2] Ability to make factories immutable by default --- .../Factories/RecipeFactoryImmutable.php | 46 +++++++++++++++++++ src/BaseFactory.php | 16 ++++++- tests/FactoryTest.php | 34 ++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 example/tests/Factories/RecipeFactoryImmutable.php diff --git a/example/tests/Factories/RecipeFactoryImmutable.php b/example/tests/Factories/RecipeFactoryImmutable.php new file mode 100644 index 0000000..6ffef4c --- /dev/null +++ b/example/tests/Factories/RecipeFactoryImmutable.php @@ -0,0 +1,46 @@ +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', + ]); + } +} diff --git a/src/BaseFactory.php b/src/BaseFactory.php index 77df657..56c948f 100644 --- a/src/BaseFactory.php +++ b/src/BaseFactory.php @@ -14,6 +14,8 @@ abstract class BaseFactory implements FactoryInterface protected string $modelClass; + protected bool $immutable = false; + protected Collection $relatedModelFactories; protected Generator $faker; @@ -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( @@ -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 diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 534a321..3dd8ac1 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -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; @@ -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 { From a69d318da043aa49090ac533cd008874d744523d Mon Sep 17 00:00:00 2001 From: Karel FAILLE Date: Sun, 21 Feb 2021 01:55:39 +0100 Subject: [PATCH 2/2] Update README.md --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 88a75bb..eab7808 100644 --- a/README.md +++ b/README.md @@ -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(); ``` @@ -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: @@ -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.