From b10fb7404b4a392cadabcc651ec7cb008382c093 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 5 May 2020 19:50:06 +0000 Subject: [PATCH 01/25] Initial Fabricator class --- system/Language/en/Fabricator.php | 20 ++ system/Test/Fabricator.php | 360 ++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 system/Language/en/Fabricator.php create mode 100644 system/Test/Fabricator.php diff --git a/system/Language/en/Fabricator.php b/system/Language/en/Fabricator.php new file mode 100644 index 000000000000..4e45dc1d6747 --- /dev/null +++ b/system/Language/en/Fabricator.php @@ -0,0 +1,20 @@ + 'Invalid model supplied for fabrication.', + 'missingFormatters' => 'No valid formatters defined.', +]; diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php new file mode 100644 index 000000000000..6c9c3ce68d3c --- /dev/null +++ b/system/Test/Fabricator.php @@ -0,0 +1,360 @@ + formatter + * + * @throws \InvalidArgumentException + */ + public function __construct($model, string $locale = null, array $formatters = null) + { + if (is_string($model)) + { + $model = new $model(); + } + + // Verify the model + if (! $model instanceof Model) + { + throw new \InvalidArgumentException(lang('Fabricator.invalidModel')); + } + + // If no locale was specified then use the App default + if (is_null($locale)) + { + $locale = config('App')->defaultLocale; + } + + // Will throw \InvalidArgumentException for unmatched locales + $this->faker = Factory::create($locale); + + // Set the formatters + $this->setFormatters($formatters); + } + + //-------------------------------------------------------------------- + + /** + * Returns the model instance + * + * @return CodeIgniter\Model + */ + public function getModel(): Model + { + return $this->model; + } + + /** + * Returns the Faker generator + * + * @return Faker\Generator + */ + public function getFaker(): Faker + { + return $this->faker; + } + + //-------------------------------------------------------------------- + + /** + * Returns the current formatters + * + * @return array|null + */ + public function getFormatters(): ?array + { + return $this->formatters; + } + + /** + * Set the formatters to use. Will attempt to autodetect if none are available. + * + * @return $this + */ + public function setFormatters(array $formatters = null): self + { + if (! is_null($formatters)) + { + $this->formatters = $formatters; + } + elseif (method_exists($this->model, 'fake')) + { + $this->formatters = null; + } + else + { + $formatters = $this->detectFormatters(); + } + + return $this->faker; + } + + /** + * Try to identify the appropriate Faker formatter for each field. + * + * @return $this + */ + protected function detectFormatters(): self + { + $this->formatters = []; + + foreach ($this->model->allowedFields as $field) + { + $this->formatters[$field] = $this->guessFormatter($field); + } + + return $this; + } + + /** + * Guess at the correct formatter to match a field name. + * + * @param $field Name of the field + * + * @return string Name of the formatter + */ + protected function guessFormatter($field): self + { + // First check for a Faker formatter of the same name - covers things like "email" + try + { + $this->faker->getFormatter($field); + return $field; + } + catch (\InvalidArgumentException $e) + { + // No match, keep going + } + + // Check some common partials + foreach (['email', 'name', 'title', 'text', 'date', 'url'] as $term) + { + if (stripos($field, $term) !== false) + { + return $field; + } + } + + if (stripos($field, 'phone') !== false) + { + return 'phoneNumber'; + } + + // Nothing left, use the default + return $this->defaultFormatter; + } + + //-------------------------------------------------------------------- + + /** + * Generate new entities with faked data + * + * @param int|null $count Optional number to create a collection + * @param array $override Array of data to add/override + * + * @return array|object An array or object (based on returnType), or an array of returnTypes + */ + public function make(int $count = null, array $override = []) + { + // If a singleton was requested then go straight to it + if (is_null($count)) + { + return $this->model->returnType == 'array' ? + $this->makeArray($override) : + $this->makeObject($override); + } + + $return = []; + + for ($i = 0; $i < $count; $i++) + { + $return[] = $this->model->returnType == 'array' ? + $this->makeArray($override) : + $this->makeObject($override); + } + + return $return; + } + + /** + * Generate an array of faked data + * + * @param array $override Array of data to add/override + * + * @return array An array of faked data + * + * @throws \RuntimeException + */ + protected function makeArray(array $override = []) + { + if (! is_null($this->formatters)) + { + $result = []; + + foreach ($this->formatters as $field => $formatter) + { + $result[$field] = $this->faker->{$formatter}; + } + } + + // If no formatters were defined then look for a model fake() method + elseif (method_exists($this->model, 'fake')) + { + $result = $this->model->fake(); + + // This should cover entities + if (method_exists($result, 'toArray')) + { + $result = $result->toArray(); + } + // Try to cast it + else + { + $result = (array) $result; + } + } + + // Nothing left to do but give up + else + { + throw new \RuntimeException(lang('Fabricator.missingFormatters')); + } + + // Replace overridden fields + return array_merge($result, $override); + } + + /** + * Generate an object of faked data + * + * @param array $override Array of data to add/override + * + * @return array An array of faked data + * + * @throws \RuntimeException + */ + protected function makeObject(array $override = []) + { + $class = $this->model->returnType; + + // If using the model's fake() method then check it for the correct return type + if (is_null($this->formatters) && method_exists($this->model, 'fake')) + { + $result = $this->model->fake(); + + if ($result instanceof $class) + { + // Set overrides manually + foreach ($override as $key => $value) + { + $result->{$key} = $value; + } + + return $result; + } + } + + // Get the array values and format them as returnType + $array = $this->makeArray($override); + $object = new $class(); + + // Check for the entity method + if (method_exists($object, 'fill')) + { + $object->fill($array); + } + else + { + foreach ($result as $key => $value) + { + $object->{$key} = $value; + } + } + + return $object; + } +} + From 02d74b61d3223389c6731c9fe2494f698920e370 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 5 May 2020 19:50:37 +0000 Subject: [PATCH 02/25] Initial Fabricator class --- system/Language/en/Fabricator.php | 4 ++-- system/Test/Fabricator.php | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/system/Language/en/Fabricator.php b/system/Language/en/Fabricator.php index 4e45dc1d6747..a3efbde2a114 100644 --- a/system/Language/en/Fabricator.php +++ b/system/Language/en/Fabricator.php @@ -15,6 +15,6 @@ */ return [ - 'invalidModel' => 'Invalid model supplied for fabrication.', - 'missingFormatters' => 'No valid formatters defined.', + 'invalidModel' => 'Invalid model supplied for fabrication.', + 'missingFormatters' => 'No valid formatters defined.', ]; diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 6c9c3ce68d3c..ee22c32aff75 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -83,9 +83,9 @@ class Fabricator /** * Store the model instance and initialize Faker to the locale. * - * @param string|Model $model Instance or classname of the model to use - * @param string|null $locale Locale for Faker provider - * @param array $formatters Array of property => formatter + * @param string|Model $model Instance or classname of the model to use + * @param string|null $locale Locale for Faker provider + * @param array $formatters Array of property => formatter * * @throws \InvalidArgumentException */ @@ -109,7 +109,7 @@ public function __construct($model, string $locale = null, array $formatters = n } // Will throw \InvalidArgumentException for unmatched locales - $this->faker = Factory::create($locale); + $this->faker = Factory::create($locale); // Set the formatters $this->setFormatters($formatters); @@ -232,8 +232,8 @@ protected function guessFormatter($field): self /** * Generate new entities with faked data * - * @param int|null $count Optional number to create a collection - * @param array $override Array of data to add/override + * @param integer|null $count Optional number to create a collection + * @param array $override Array of data to add/override * * @return array|object An array or object (based on returnType), or an array of returnTypes */ @@ -242,16 +242,16 @@ public function make(int $count = null, array $override = []) // If a singleton was requested then go straight to it if (is_null($count)) { - return $this->model->returnType == 'array' ? + return $this->model->returnType === 'array' ? $this->makeArray($override) : $this->makeObject($override); } $return = []; - + for ($i = 0; $i < $count; $i++) { - $return[] = $this->model->returnType == 'array' ? + $return[] = $this->model->returnType === 'array' ? $this->makeArray($override) : $this->makeObject($override); } @@ -262,7 +262,7 @@ public function make(int $count = null, array $override = []) /** * Generate an array of faked data * - * @param array $override Array of data to add/override + * @param array $override Array of data to add/override * * @return array An array of faked data * @@ -310,7 +310,7 @@ protected function makeArray(array $override = []) /** * Generate an object of faked data * - * @param array $override Array of data to add/override + * @param array $override Array of data to add/override * * @return array An array of faked data * @@ -324,7 +324,7 @@ protected function makeObject(array $override = []) if (is_null($this->formatters) && method_exists($this->model, 'fake')) { $result = $this->model->fake(); - + if ($result instanceof $class) { // Set overrides manually @@ -357,4 +357,3 @@ protected function makeObject(array $override = []) return $object; } } - From 9968ce9445d9c3e8c3ca632271ea146e0b891e90 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 6 May 2020 16:21:05 +0000 Subject: [PATCH 03/25] Add create methods --- system/Test/Fabricator.php | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index ee22c32aff75..e45392bb8781 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -356,4 +356,86 @@ protected function makeObject(array $override = []) return $object; } + + //-------------------------------------------------------------------- + + /** + * Generate new entities from the database + * + * @param integer|null $count Optional number to create a collection + * @param array $override Array of data to add/override + * @param boolean $mock Whether to execute or mock the insertion + * + * @return array|object An array or object (based on returnType), or an array of returnTypes + */ + public function create(int $count = null, array $override = [], bool $mock = false) + { + // Intercept mock requests + if ($mock) + { + return $this->createMock($count, $override); + } + + $ids = []; + + // Iterate over new entities and insert each one, storing insert IDs + foreach ($this->make($count ?? 1, $override) as $result) + { + $ids[] = $this->model->insert($row, true); + } + + return $this->model->find(is_null($count) ? reset($ids) : $ids); + } + + /** + * Generate new database entities without actually inserting them + * + * @param integer|null $count Optional number to create a collection + * @param array $override Array of data to add/override + * + * @return array|object An array or object (based on returnType), or an array of returnTypes + */ + protected function createMock(int $count = null, array $override = []) + { + $datetime = $this->model->setDate(); + + // Determine which fields we will need + $fields = []; + + if ($this->model->useTimestamps) + { + $fields[$this->model->createdField] = $datetime; + $fields[$this->model->updatedField] = $datetime; + } + + if ($this->model->useSoftDeletes) + { + $fields[$this->model->deletedField] = $datetime; + } + + // Iterate over new entities and add the necessary fields + $return = []; + foreach ($this->make($count ?? 1, $override) as $i => $result) + { + // Set the ID + $fields[$this->model->primaryKey] = $i; + + // Merge fields + if (is_array($result)) + { + $result = array_merge($result, $fields); + } + else + { + foreach ($fields as $key => $value) + { + $result->{$key} = $value; + } + } + + $return[] = $result; + } + + return is_null($count) ? reset($return) : $return; + } } From 1b59294be6eeb65a13b393fe0f6fa41942a8418a Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 6 May 2020 16:30:43 +0000 Subject: [PATCH 04/25] Begin tests --- composer.json | 1 + tests/system/Test/FabricatorTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/system/Test/FabricatorTest.php diff --git a/composer.json b/composer.json index a8045d379f79..e0b6a18e0767 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ }, "require-dev": { "codeigniter4/codeigniter4-standard": "^1.0", + "fzaninotto/faker": "^1.9@dev", "mikey179/vfsstream": "1.6.*", "phpunit/phpunit": "^8.5", "squizlabs/php_codesniffer": "^3.3" diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php new file mode 100644 index 000000000000..551e3740fe2b --- /dev/null +++ b/tests/system/Test/FabricatorTest.php @@ -0,0 +1,14 @@ +assertInstanceOf('CodeIgniter\Test\Fabricator', $fabricator); + } +} From f6b6c80bed911a1120c2fb60d97bb497205c97b0 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 6 May 2020 20:01:47 +0000 Subject: [PATCH 05/25] Begin tests --- tests/system/Test/FabricatorTest.php | 111 ++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 551e3740fe2b..645354cca982 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -1,14 +1,117 @@ 'name', + 'email' => 'email', + 'country' => 'country', + 'deleted_at' => 'datetime', + ]; + + public function testConstructorWithString() { $fabricator = new Fabricator(UserModel::class); - $this->assertInstanceOf('CodeIgniter\Test\Fabricator', $fabricator); + $this->assertInstanceOf(Fabricator::class, $fabricator); + } + + public function testConstructorWithInstance() + { + $model = new UserModel(); + + $fabricator = new Fabricator($model); + + $this->assertInstanceOf(Fabricator::class, $fabricator); + } + + public function testConstructorSetsFormatters() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $this->assertEquals($this->formatters, $fabricator->getFormatters()); + } + + public function testConstructorGuessesFormatters() + { + $fabricator = new Fabricator(UserModel::class, null); + + $this->assertEquals($this->formatters, $fabricator->getFormatters()); } + + public function testConstructorDefaultsToAppLocale() + { + $fabricator = new Fabricator(UserModel::class); + + $this->assertEquals(config('App')->defaultLocale, $fabricator->getLocale()); + } + + public function testConstructorUsesProvidedLocale() + { + $locale = 'fr_FR'; + + $fabricator = new Fabricator(UserModel::class, null, $locale); + + $this->assertEquals($locale, $fabricator->getLocale()); + } + + public function testGetModelReturnsModel() + { + $fabricator = new Fabricator(UserModel::class); + $this->assertInstanceOf(UserModel::class, $fabricator->getModel()); + + $model = new UserModel(); + $fabricator2 = new Fabricator($model); + $this->assertInstanceOf(UserModel::class, $fabricator2->getModel()); + } + + public function testGetFakerReturnsUsableGenerator() + { + $fabricator = new Fabricator(UserModel::class); + + $faker = $fabricator->getFaker(); + + $this->assertIsNumeric($faker->randomDigit); + } + + public function testSetFormattersChangesFormatters() + { + $formatters = ['boo' => 'hiss']; + $fabricator = new Fabricator(UserModel::class); + + $fabricator->setFormatters($formatters); + + $this->assertEquals($formatters, $fabricator->getFormatters()); + } + + public function testSetFormattersDetectsFormatters() + { + $formatters = ['boo' => 'hiss']; + $fabricator = new Fabricator(UserModel::class, $formatters); + + $fabricator->setFormatters(); + + $this->assertEquals($this->formatters, $fabricator->getFormatters()); + } + + public function testDetectFormattersDetectsFormatters() + { + $formatters = ['boo' => 'hiss']; + $fabricator = new Fabricator(UserModel::class, $formatters); + + $method = $this->getPrivateMethodInvoker($fabricator, 'detectFormatters'); + + $method(); + + $this->assertEquals($this->formatters, $fabricator->getFormatters()); + } + } From 014e1dd6ac45bebcab8b7907704e71da9773f059 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 6 May 2020 20:02:17 +0000 Subject: [PATCH 06/25] Bugfix test issues --- system/Test/Fabricator.php | 50 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index e45392bb8781..a8c3aee953cc 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -64,6 +64,13 @@ class Fabricator */ protected $model; + /** + * Locale used to initialize Faker + * + * @var string + */ + protected $locale; + /** * Map of properties and their formatter to use * @@ -84,12 +91,12 @@ class Fabricator * Store the model instance and initialize Faker to the locale. * * @param string|Model $model Instance or classname of the model to use + * @param array|null $formatters Array of property => formatter * @param string|null $locale Locale for Faker provider - * @param array $formatters Array of property => formatter * * @throws \InvalidArgumentException */ - public function __construct($model, string $locale = null, array $formatters = null) + public function __construct($model, array $formatters = null, string $locale = null) { if (is_string($model)) { @@ -102,14 +109,19 @@ public function __construct($model, string $locale = null, array $formatters = n throw new \InvalidArgumentException(lang('Fabricator.invalidModel')); } + $this->model = $model; + // If no locale was specified then use the App default if (is_null($locale)) { $locale = config('App')->defaultLocale; } - // Will throw \InvalidArgumentException for unmatched locales - $this->faker = Factory::create($locale); + // There is no easy way to retrieve the locale from Faker so we will store it + $this->locale = $locale; + + // Create the locale-specific Generator + $this->faker = Factory::create($this->locale); // Set the formatters $this->setFormatters($formatters); @@ -127,12 +139,22 @@ public function getModel(): Model return $this->model; } + /** + * Returns the locale + * + * @return string + */ + public function getLocale(): string + { + return $this->locale; + } + /** * Returns the Faker generator * * @return Faker\Generator */ - public function getFaker(): Faker + public function getFaker(): Generator { return $this->faker; } @@ -169,7 +191,7 @@ public function setFormatters(array $formatters = null): self $formatters = $this->detectFormatters(); } - return $this->faker; + return $this; } /** @@ -196,7 +218,7 @@ protected function detectFormatters(): self * * @return string Name of the formatter */ - protected function guessFormatter($field): self + protected function guessFormatter($field): string { // First check for a Faker formatter of the same name - covers things like "email" try @@ -209,6 +231,16 @@ protected function guessFormatter($field): self // No match, keep going } + // Next look for known model fields + if (in_array($field, [$this->model->createdField, $this->model->updatedField, $this->model->deletedField])) + { + return $this->model->dateFormat; + } + elseif ($field === $this->model->primaryKey) + { + return 'numberBetween'; + } + // Check some common partials foreach (['email', 'name', 'title', 'text', 'date', 'url'] as $term) { @@ -318,7 +350,7 @@ protected function makeArray(array $override = []) */ protected function makeObject(array $override = []) { - $class = $this->model->returnType; + $class = $this->model->returnType === 'object' ? 'stdClass' : $this->model->returnType; // If using the model's fake() method then check it for the correct return type if (is_null($this->formatters) && method_exists($this->model, 'fake')) @@ -348,7 +380,7 @@ protected function makeObject(array $override = []) } else { - foreach ($result as $key => $value) + foreach ($array as $key => $value) { $object->{$key} = $value; } From c845b3c5ba6e4bf5a4d014f151b4b408c74b706a Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 7 May 2020 17:01:29 +0000 Subject: [PATCH 07/25] Add model with fake method, more tests --- system/Test/Fabricator.php | 6 +- tests/_support/Models/FabricatorModel.php | 29 ++++++ tests/system/Test/FabricatorTest.php | 112 ++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/_support/Models/FabricatorModel.php diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index a8c3aee953cc..4abb0a8d5c4f 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -246,7 +246,7 @@ protected function guessFormatter($field): string { if (stripos($field, $term) !== false) { - return $field; + return $term; } } @@ -315,7 +315,7 @@ protected function makeArray(array $override = []) // If no formatters were defined then look for a model fake() method elseif (method_exists($this->model, 'fake')) { - $result = $this->model->fake(); + $result = $this->model->fake($this->faker); // This should cover entities if (method_exists($result, 'toArray')) @@ -355,7 +355,7 @@ protected function makeObject(array $override = []) // If using the model's fake() method then check it for the correct return type if (is_null($this->formatters) && method_exists($this->model, 'fake')) { - $result = $this->model->fake(); + $result = $this->model->fake($this->faker); if ($result instanceof $class) { diff --git a/tests/_support/Models/FabricatorModel.php b/tests/_support/Models/FabricatorModel.php new file mode 100644 index 000000000000..4e987e5ea526 --- /dev/null +++ b/tests/_support/Models/FabricatorModel.php @@ -0,0 +1,29 @@ + $faker->ipv4, + 'description' => $faker->words(10), + ]; + } +} diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 645354cca982..85d3e3bb47c0 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -1,6 +1,7 @@ 'datetime', ]; + //-------------------------------------------------------------------- + public function testConstructorWithString() { $fabricator = new Fabricator(UserModel::class); @@ -63,6 +66,8 @@ public function testConstructorUsesProvidedLocale() $this->assertEquals($locale, $fabricator->getLocale()); } + //-------------------------------------------------------------------- + public function testGetModelReturnsModel() { $fabricator = new Fabricator(UserModel::class); @@ -82,6 +87,8 @@ public function testGetFakerReturnsUsableGenerator() $this->assertIsNumeric($faker->randomDigit); } + //-------------------------------------------------------------------- + public function testSetFormattersChangesFormatters() { $formatters = ['boo' => 'hiss']; @@ -114,4 +121,109 @@ public function testDetectFormattersDetectsFormatters() $this->assertEquals($this->formatters, $fabricator->getFormatters()); } + //-------------------------------------------------------------------- + + public function testGuessFormattersReturnsActual() + { + $fabricator = new Fabricator(UserModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'guessFormatter'); + + $field = 'catchPhrase'; + $formatter = $method($field); + + $this->assertEquals($field, $formatter); + } + + public function testGuessFormattersFieldReturnsDateFormat() + { + $fabricator = new Fabricator(UserModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'guessFormatter'); + + $field = 'created_at'; + $formatter = $method($field); + + $this->assertEquals($fabricator->getModel()->dateFormat, $formatter); + } + + public function testGuessFormattersPrimaryReturnsNumberBetween() + { + $fabricator = new Fabricator(UserModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'guessFormatter'); + + $field = 'id'; + $formatter = $method($field); + + $this->assertEquals('numberBetween', $formatter); + } + + public function testGuessFormattersMatchesPartial() + { + $fabricator = new Fabricator(UserModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'guessFormatter'); + + $field = 'business_email'; + $formatter = $method($field); + + $this->assertEquals('email', $formatter); + } + + public function testGuessFormattersFallback() + { + $fabricator = new Fabricator(UserModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'guessFormatter'); + + $field = 'zaboomafoo'; + $formatter = $method($field); + + $this->assertEquals($fabricator->defaultFormatter, $formatter); + } + + //-------------------------------------------------------------------- + + public function testMakeArrayReturnsArray() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); + $result = $method(); + + $this->assertIsArray($result); + } + + public function testMakeArrayUsesOverride() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $override = ['name' => 'The Admiral']; + + $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); + $result = $method($override); + + $this->assertEquals($override['name'], $result['name']); + } + + public function testMakeArrayReturnsValidData() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); + $result = $method(); + + $this->assertEquals($result['email'], filter_var($result['email'], FILTER_VALIDATE_EMAIL)); + } + + public function testMakeArrayUsesFakeMethod() + { + $fabricator = new Fabricator(FabricatorModel::class); + + $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); + $result = $method(); + + $this->assertEquals($result['name'], filter_var($result['name'], FILTER_VALIDATE_IP)); + } } From e93fcce48207ef8735e44f9f59123302fbb13769 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 7 May 2020 17:15:09 +0000 Subject: [PATCH 08/25] Remove requirement for CodeIgniter\Model --- system/Test/Fabricator.php | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 4abb0a8d5c4f..3ccd19e51e89 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -58,9 +58,9 @@ class Fabricator protected $faker; /** - * Model instance + * Model instance (can be non-framework if it follows framework design) * - * @var \CodeIgniter\Model + * @var CodeIgniter\Model|object */ protected $model; @@ -90,9 +90,9 @@ class Fabricator /** * Store the model instance and initialize Faker to the locale. * - * @param string|Model $model Instance or classname of the model to use - * @param array|null $formatters Array of property => formatter - * @param string|null $locale Locale for Faker provider + * @param string|object $model Instance or classname of the model to use + * @param array|null $formatters Array of property => formatter + * @param string|null $locale Locale for Faker provider * * @throws \InvalidArgumentException */ @@ -100,13 +100,7 @@ public function __construct($model, array $formatters = null, string $locale = n { if (is_string($model)) { - $model = new $model(); - } - - // Verify the model - if (! $model instanceof Model) - { - throw new \InvalidArgumentException(lang('Fabricator.invalidModel')); + $model = model($model); } $this->model = $model; @@ -132,9 +126,9 @@ public function __construct($model, array $formatters = null, string $locale = n /** * Returns the model instance * - * @return CodeIgniter\Model + * @return object Framework or compatible model */ - public function getModel(): Model + public function getModel() { return $this->model; } From 8d89c6e971c267579cc9ae0b3e569b13444c1475 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 7 May 2020 19:39:46 +0000 Subject: [PATCH 09/25] Move overrides to separate method --- system/Test/Fabricator.php | 95 +++++++++++++++++++++++----- tests/system/Test/FabricatorTest.php | 45 +++++++++++-- 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 3ccd19e51e89..f65fb7a39810 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -78,6 +78,20 @@ class Fabricator */ protected $formatters; + /** + * Array of data to add or override faked versions + * + * @var array + */ + protected $overrides = []; + + /** + * Array of single-use data to override faked versions + * + * @var array|null + */ + protected $tmpOverrides; + /** * Default formatter to use when nothing is detected * @@ -121,6 +135,22 @@ public function __construct($model, array $formatters = null, string $locale = n $this->setFormatters($formatters); } + /** + * Reset state to defaults + * + * @return $this + */ + public function reset(): self + { + $this->setFormatters(); + + $this->overrides = $this->tmpOverrides = []; + $this->locale = config('App')->defaultLocale; + $this->faker = Factory::create($this->locale); + + return $this; + } + //-------------------------------------------------------------------- /** @@ -155,6 +185,42 @@ public function getFaker(): Generator //-------------------------------------------------------------------- + /** + * Return and reset tmpOverrides + * + * @return array + */ + public function getOverrides(): array + { + $overrides = $this->tmpOverrides ?? $this->overrides; + + $this->tmpOverrides = $this->overrides; + + return $overrides; + } + + /** + * Set the overrides, once or persistent + * + * @param array $overrides Array of [field => value] + * @param boolean $persist Whether these overrides should persist through the next operation + * + * @return $this + */ + public function setOverrides(array $overrides = [], $persist = true): self + { + if ($persist) + { + $this->overrides = $overrides; + } + + $this->tmpOverrides = $overrides; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Returns the current formatters * @@ -168,6 +234,8 @@ public function getFormatters(): ?array /** * Set the formatters to use. Will attempt to autodetect if none are available. * + * @param array|null $formatters Array of [field => formatter], or null to detect + * * @return $this */ public function setFormatters(array $formatters = null): self @@ -258,19 +326,18 @@ protected function guessFormatter($field): string /** * Generate new entities with faked data * - * @param integer|null $count Optional number to create a collection - * @param array $override Array of data to add/override + * @param integer|null $count Optional number to create a collection * * @return array|object An array or object (based on returnType), or an array of returnTypes */ - public function make(int $count = null, array $override = []) + public function make(int $count = null) { // If a singleton was requested then go straight to it if (is_null($count)) { return $this->model->returnType === 'array' ? - $this->makeArray($override) : - $this->makeObject($override); + $this->makeArray() : + $this->makeObject(); } $return = []; @@ -278,8 +345,8 @@ public function make(int $count = null, array $override = []) for ($i = 0; $i < $count; $i++) { $return[] = $this->model->returnType === 'array' ? - $this->makeArray($override) : - $this->makeObject($override); + $this->makeArray() : + $this->makeObject(); } return $return; @@ -288,13 +355,11 @@ public function make(int $count = null, array $override = []) /** * Generate an array of faked data * - * @param array $override Array of data to add/override - * * @return array An array of faked data * * @throws \RuntimeException */ - protected function makeArray(array $override = []) + protected function makeArray() { if (! is_null($this->formatters)) { @@ -330,19 +395,17 @@ protected function makeArray(array $override = []) } // Replace overridden fields - return array_merge($result, $override); + return array_merge($result, $this->getOverrides()); } /** * Generate an object of faked data * - * @param array $override Array of data to add/override - * * @return array An array of faked data * * @throws \RuntimeException */ - protected function makeObject(array $override = []) + protected function makeObject() { $class = $this->model->returnType === 'object' ? 'stdClass' : $this->model->returnType; @@ -354,7 +417,7 @@ protected function makeObject(array $override = []) if ($result instanceof $class) { // Set overrides manually - foreach ($override as $key => $value) + foreach ($this->getOverrides() as $key => $value) { $result->{$key} = $value; } @@ -364,7 +427,7 @@ protected function makeObject(array $override = []) } // Get the array values and format them as returnType - $array = $this->makeArray($override); + $array = $this->makeArray(); $object = new $class(); // Check for the entity method diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 85d3e3bb47c0..10cb798619fd 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -123,6 +123,40 @@ public function testDetectFormattersDetectsFormatters() //-------------------------------------------------------------------- + public function testSetOverridesSets() + { + $overrides = ['name' => 'Steve']; + $fabricator = new Fabricator(UserModel::class); + + $fabricator->setOverrides($overrides); + + $this->assertEquals($overrides, $fabricator->getOverrides()); + } + + public function testSetOverridesDefaultPersists() + { + $overrides = ['name' => 'Steve']; + $fabricator = new Fabricator(UserModel::class); + + $fabricator->setOverrides($overrides); + $fabricator->getOverrides(); + + $this->assertEquals($overrides, $fabricator->getOverrides()); + } + + public function testSetOverridesOnce() + { + $overrides = ['name' => 'Steve']; + $fabricator = new Fabricator(UserModel::class); + + $fabricator->setOverrides($overrides, false); + $fabricator->getOverrides(); + + $this->assertEquals([], $fabricator->getOverrides()); + } + + //-------------------------------------------------------------------- + public function testGuessFormattersReturnsActual() { $fabricator = new Fabricator(UserModel::class); @@ -195,16 +229,17 @@ public function testMakeArrayReturnsArray() $this->assertIsArray($result); } - public function testMakeArrayUsesOverride() + public function testMakeArrayUsesOverrides() { - $fabricator = new Fabricator(UserModel::class, $this->formatters); + $overrides = ['name' => 'The Admiral']; - $override = ['name' => 'The Admiral']; + $fabricator = new Fabricator(UserModel::class, $this->formatters); + $fabricator->setOverrides($overrides); $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); - $result = $method($override); + $result = $method(); - $this->assertEquals($override['name'], $result['name']); + $this->assertEquals($overrides['name'], $result['name']); } public function testMakeArrayReturnsValidData() From 236c49ad812fc47f3f6eac28538482681d4a1c78 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 7 May 2020 20:21:11 +0000 Subject: [PATCH 10/25] Rework makeX methods to be public --- system/Test/Fabricator.php | 38 ++++++++----- tests/system/Test/FabricatorTest.php | 84 +++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index f65fb7a39810..b450385cbc21 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -335,18 +335,18 @@ public function make(int $count = null) // If a singleton was requested then go straight to it if (is_null($count)) { - return $this->model->returnType === 'array' ? - $this->makeArray() : - $this->makeObject(); + return $this->model->returnType === 'array' + ? $this->makeArray() + : $this->makeObject(); } $return = []; for ($i = 0; $i < $count; $i++) { - $return[] = $this->model->returnType === 'array' ? - $this->makeArray() : - $this->makeObject(); + $return[] = $this->model->returnType === 'array' + ? $this->makeArray() + : $this->makeObject(); } return $return; @@ -359,7 +359,7 @@ public function make(int $count = null) * * @throws \RuntimeException */ - protected function makeArray() + public function makeArray() { if (! is_null($this->formatters)) { @@ -401,20 +401,32 @@ protected function makeArray() /** * Generate an object of faked data * - * @return array An array of faked data + * @param string|null $className Class name of the object to create; null to use model default + * + * @return object An instance of the class with faked data * * @throws \RuntimeException */ - protected function makeObject() + public function makeObject(string $className = null): object { - $class = $this->model->returnType === 'object' ? 'stdClass' : $this->model->returnType; + if (is_null($className)) + { + if ($this->model->returnType === 'object' || $this->model->returnType === 'array') + { + $className = 'stdClass'; + } + else + { + $className = $this->model->returnType; + } + } // If using the model's fake() method then check it for the correct return type if (is_null($this->formatters) && method_exists($this->model, 'fake')) { $result = $this->model->fake($this->faker); - if ($result instanceof $class) + if ($result instanceof $className) { // Set overrides manually foreach ($this->getOverrides() as $key => $value) @@ -426,9 +438,9 @@ protected function makeObject() } } - // Get the array values and format them as returnType + // Get the array values and apply them to the object $array = $this->makeArray(); - $object = new $class(); + $object = new $className(); // Check for the entity method if (method_exists($object, 'fill')) diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 10cb798619fd..0ceda21239b9 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -1,6 +1,8 @@ formatters); - $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); - $result = $method(); + $result = $fabricator->makeArray(); $this->assertIsArray($result); } @@ -236,8 +237,7 @@ public function testMakeArrayUsesOverrides() $fabricator = new Fabricator(UserModel::class, $this->formatters); $fabricator->setOverrides($overrides); - $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); - $result = $method(); + $result = $fabricator->makeArray(); $this->assertEquals($overrides['name'], $result['name']); } @@ -246,8 +246,7 @@ public function testMakeArrayReturnsValidData() { $fabricator = new Fabricator(UserModel::class, $this->formatters); - $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); - $result = $method(); + $result = $fabricator->makeArray(); $this->assertEquals($result['email'], filter_var($result['email'], FILTER_VALIDATE_EMAIL)); } @@ -256,9 +255,78 @@ public function testMakeArrayUsesFakeMethod() { $fabricator = new Fabricator(FabricatorModel::class); - $method = $this->getPrivateMethodInvoker($fabricator, 'makeArray'); - $result = $method(); + $result = $fabricator->makeArray(); $this->assertEquals($result['name'], filter_var($result['name'], FILTER_VALIDATE_IP)); } + + //-------------------------------------------------------------------- + + public function testMakeObjectReturnsModelReturnType() + { + $fabricator = new Fabricator(EntityModel::class, $this->formatters); + $expected = $fabricator->getModel()->returnType; + + $result = $fabricator->makeObject(); + + $this->assertInstanceOf($expected, $result); + } + + public function testMakeObjectReturnsProvidedClass() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + $className = 'Tests\Support\Models\SimpleEntity'; + + $result = $fabricator->makeObject($className); + + $this->assertInstanceOf($className, $result); + } + + public function testMakeObjectReturnsStdClassForArrayReturnType() + { + $fabricator = new Fabricator(EventModel::class, $this->formatters); + + $result = $fabricator->makeObject(); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testMakeObjectReturnsStdClassForObjectReturnType() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $result = $fabricator->makeObject(); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testMakeObjectUsesOverrides() + { + $overrides = ['name' => 'The Admiral']; + + $fabricator = new Fabricator(UserModel::class, $this->formatters); + $fabricator->setOverrides($overrides); + + $result = $fabricator->makeObject(); + + $this->assertEquals($overrides['name'], $result->name); + } + + public function testMakeObjectReturnsValidData() + { + $fabricator = new Fabricator(UserModel::class, $this->formatters); + + $result = $fabricator->makeObject(); + + $this->assertEquals($result->email, filter_var($result->email, FILTER_VALIDATE_EMAIL)); + } + + public function testMakeObjectUsesFakeMethod() + { + $fabricator = new Fabricator(FabricatorModel::class); + + $result = $fabricator->makeObject(); + + $this->assertEquals($result->name, filter_var($result->name, FILTER_VALIDATE_IP)); + } } From 1b4e78e321ab01db8b1da554190f81c3aa568c20 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 7 May 2020 23:34:00 +0000 Subject: [PATCH 11/25] Clean up old references --- system/Test/Fabricator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index b450385cbc21..aa1e76c3bbf6 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -469,18 +469,18 @@ public function makeObject(string $className = null): object * * @return array|object An array or object (based on returnType), or an array of returnTypes */ - public function create(int $count = null, array $override = [], bool $mock = false) + public function create(int $count = null, bool $mock = false) { // Intercept mock requests if ($mock) { - return $this->createMock($count, $override); + return $this->createMock($count); } $ids = []; // Iterate over new entities and insert each one, storing insert IDs - foreach ($this->make($count ?? 1, $override) as $result) + foreach ($this->make($count ?? 1) as $result) { $ids[] = $this->model->insert($row, true); } @@ -516,7 +516,7 @@ protected function createMock(int $count = null, array $override = []) // Iterate over new entities and add the necessary fields $return = []; - foreach ($this->make($count ?? 1, $override) as $i => $result) + foreach ($this->make($count ?? 1) as $i => $result) { // Set the ID $fields[$this->model->primaryKey] = $i; From 18287f867a200de1a7fa95faec5a15e20b4e39e9 Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 02:07:20 +0000 Subject: [PATCH 12/25] Bugfix int formatter --- system/Test/Fabricator.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index aa1e76c3bbf6..4aa0bfabe877 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -296,7 +296,20 @@ protected function guessFormatter($field): string // Next look for known model fields if (in_array($field, [$this->model->createdField, $this->model->updatedField, $this->model->deletedField])) { - return $this->model->dateFormat; + switch ($this->model->dateFormat) + { + case 'datetime': + return 'dateTime'; + break; + + case 'date': + return 'date'; + break; + + case 'int': + return 'unixTime'; + break; + } } elseif ($field === $this->model->primaryKey) { From 34dc25ea8f5ad3a8caa47757771e4c6e582dcb4a Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 02:07:36 +0000 Subject: [PATCH 13/25] Add make() tests --- tests/system/Test/FabricatorTest.php | 30 ++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 0ceda21239b9..ebf45167a340 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -17,7 +17,7 @@ class FabricatorTest extends CIUnitTestCase 'name' => 'name', 'email' => 'email', 'country' => 'country', - 'deleted_at' => 'datetime', + 'deleted_at' => 'dateTime', ]; //-------------------------------------------------------------------- @@ -180,7 +180,7 @@ public function testGuessFormattersFieldReturnsDateFormat() $field = 'created_at'; $formatter = $method($field); - $this->assertEquals($fabricator->getModel()->dateFormat, $formatter); + $this->assertEquals('dateTime', $formatter); } public function testGuessFormattersPrimaryReturnsNumberBetween() @@ -264,7 +264,7 @@ public function testMakeArrayUsesFakeMethod() public function testMakeObjectReturnsModelReturnType() { - $fabricator = new Fabricator(EntityModel::class, $this->formatters); + $fabricator = new Fabricator(EntityModel::class); $expected = $fabricator->getModel()->returnType; $result = $fabricator->makeObject(); @@ -284,7 +284,7 @@ public function testMakeObjectReturnsProvidedClass() public function testMakeObjectReturnsStdClassForArrayReturnType() { - $fabricator = new Fabricator(EventModel::class, $this->formatters); + $fabricator = new Fabricator(EventModel::class); $result = $fabricator->makeObject(); @@ -329,4 +329,26 @@ public function testMakeObjectUsesFakeMethod() $this->assertEquals($result->name, filter_var($result->name, FILTER_VALIDATE_IP)); } + + //-------------------------------------------------------------------- + + public function testMakeReturnsSingleton() + { + $fabricator = new Fabricator(UserModel::class); + + $result = $fabricator->make(); + + $this->assertInstanceOf('stdClass', $result); + } + + public function testMakeReturnsExpectedCount() + { + $fabricator = new Fabricator(UserModel::class); + + $count = 10; + $result = $fabricator->make($count); + + $this->assertIsArray($result); + $this->assertCount($count, $result); + } } From 5c8ea561f0e760c2468ef5169c1495bc32f35243 Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 16:22:03 +0000 Subject: [PATCH 14/25] Add mock create tests --- tests/system/Test/FabricatorTest.php | 37 ++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index ebf45167a340..effdf6e0c55a 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -17,7 +17,7 @@ class FabricatorTest extends CIUnitTestCase 'name' => 'name', 'email' => 'email', 'country' => 'country', - 'deleted_at' => 'dateTime', + 'deleted_at' => 'date', ]; //-------------------------------------------------------------------- @@ -180,7 +180,7 @@ public function testGuessFormattersFieldReturnsDateFormat() $field = 'created_at'; $formatter = $method($field); - $this->assertEquals('dateTime', $formatter); + $this->assertEquals('date', $formatter); } public function testGuessFormattersPrimaryReturnsNumberBetween() @@ -351,4 +351,37 @@ public function testMakeReturnsExpectedCount() $this->assertIsArray($result); $this->assertCount($count, $result); } + + //-------------------------------------------------------------------- + + public function testCreateMockReturnsSingleton() + { + $fabricator = new Fabricator(UserModel::class); + + $result = $fabricator->create(null, true); + + $this->assertInstanceOf('stdClass', $result); + } + + public function testCreateMockReturnsExpectedCount() + { + $fabricator = new Fabricator(UserModel::class); + + $count = 10; + $result = $fabricator->create($count, true); + + $this->assertIsArray($result); + $this->assertCount($count, $result); + } + + public function testCreateMockSetsDatabaseFields() + { + $fabricator = new Fabricator(FabricatorModel::class); + + $result = $fabricator->create(null, true); + + $this->assertIsInt($result->id); + $this->assertIsInt($result->created_at); + $this->assertIsInt($result->deleted_at); + } } From 72426e08706c07eca1c6e099cc8dcc07ba45c2fe Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 16:22:23 +0000 Subject: [PATCH 15/25] Add live tests and model --- tests/_support/Models/FabricatorModel.php | 4 ++- .../Database/Live/FabricatorLiveTest.php | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/system/Database/Live/FabricatorLiveTest.php diff --git a/tests/_support/Models/FabricatorModel.php b/tests/_support/Models/FabricatorModel.php index 4e987e5ea526..68136f97d209 100644 --- a/tests/_support/Models/FabricatorModel.php +++ b/tests/_support/Models/FabricatorModel.php @@ -9,7 +9,9 @@ class FabricatorModel extends Model protected $returnType = 'object'; - protected $useSoftDeletes = false; + protected $useSoftDeletes = true; + + protected $useTimestamps = true; protected $dateFormat = 'int'; diff --git a/tests/system/Database/Live/FabricatorLiveTest.php b/tests/system/Database/Live/FabricatorLiveTest.php new file mode 100644 index 000000000000..a1ac780b4eb7 --- /dev/null +++ b/tests/system/Database/Live/FabricatorLiveTest.php @@ -0,0 +1,34 @@ +create(); + + $this->seeInDatabase('user', ['name' => $result->name]); + } + + public function testCreateAddsCountToDatabase() + { + $count = 10; + + $fabricator = new Fabricator(UserModel::class); + + $result = $fabricator->create($count); + + $this->seeNumRecords($count, 'user', []); + } + +} From 49b68652a4739199d418d5129cd610720485d10a Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 16:22:56 +0000 Subject: [PATCH 16/25] Bugfix various test findings --- system/Test/Fabricator.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 4aa0bfabe877..4689e50223e1 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -299,7 +299,7 @@ protected function guessFormatter($field): string switch ($this->model->dateFormat) { case 'datetime': - return 'dateTime'; + return 'date'; break; case 'date': @@ -495,23 +495,29 @@ public function create(int $count = null, bool $mock = false) // Iterate over new entities and insert each one, storing insert IDs foreach ($this->make($count ?? 1) as $result) { - $ids[] = $this->model->insert($row, true); + $ids[] = $this->model->insert($result, true); } - return $this->model->find(is_null($count) ? reset($ids) : $ids); + return $this->model->withDeleted()->find(is_null($count) ? reset($ids) : $ids); } /** * Generate new database entities without actually inserting them * - * @param integer|null $count Optional number to create a collection - * @param array $override Array of data to add/override + * @param integer|null $count Optional number to create a collection * * @return array|object An array or object (based on returnType), or an array of returnTypes */ - protected function createMock(int $count = null, array $override = []) + protected function createMock(int $count = null) { - $datetime = $this->model->setDate(); + switch ($this->model->dateFormat) + { + case 'datetime': + $datetime = date('Y-m-d H:i:s'); + case 'date': + $datetime = date('Y-m-d'); + default: + $datetime = time(); } // Determine which fields we will need $fields = []; From 8cedf69c898b58090aa184a34abb3a0c649b31dc Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 20:11:25 +0000 Subject: [PATCH 17/25] Add Fabricator docs --- tests/_support/Models/FabricatorModel.php | 2 +- user_guide_src/source/testing/fabricator.rst | 145 +++++++++++++++++++ user_guide_src/source/testing/index.rst | 1 + 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/testing/fabricator.rst diff --git a/tests/_support/Models/FabricatorModel.php b/tests/_support/Models/FabricatorModel.php index 68136f97d209..de99daece4b2 100644 --- a/tests/_support/Models/FabricatorModel.php +++ b/tests/_support/Models/FabricatorModel.php @@ -21,7 +21,7 @@ class FabricatorModel extends Model ]; // Return a faked entity - public function fake(Generator &$faker = null) + public function fake(Generator &$faker) { return (object) [ 'name' => $faker->ipv4, diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst new file mode 100644 index 000000000000..ec51f2fe590b --- /dev/null +++ b/user_guide_src/source/testing/fabricator.rst @@ -0,0 +1,145 @@ +#################### +Generating Test Data +#################### + +Often you will need sample data for your application to run its tests. The ``Fabricator`` class +uses fzaninotto's `Faker `_ to turn models into generators +of random data. Use fabricators in your seeds or test cases to stage fake data for your unit tests. + +Loading Fabricators +=================== + +At its most basic a fabricator takes the model to act on:: + + use App\Models\UserModel; + use CodeIgniter\Test\Fabricator; + + $fabricator = new Fabricator(UserModel::class); + +The parameter can be a string specifying the name of the model, or an instance of the model itself:: + + $model = new UserModel($testDbConnection); + + $fabricator = new Fabricator($model); + +Defining Formatters +=================== + +Faker generates data by requesting it from a formatter. With no formatters defined, ``Fabricator`` will +attempt to guess at the most appropriate fit based on the field name and properties of the model it +represents, falling back on ``$fabricator->defaultFormatter``. This may be fine if your field names +correspond with common formatters, or if you don't care much about the content of the fields, but most +of the time you will want to specify the formatters to use as the second parameter to the constructor:: + + $formatters = [ + 'first' => 'firstName', + 'email' => 'email', + 'phone' => 'phoneNumber', + 'avatar' => 'imageUrl', + ]; + + $fabricator = new Fabricator(UserModel::class, $formatters); + +You can also change the formatters after a fabricator is initialized by using the ``setFormatters()`` method. + +**Advanced Formatting** + +Sometimes the default return of a formatter is not enough. Faker providers allow parameters to most formatters +to further limit the scope of random data. A fabricator will check its representative model for the ``fake()`` +method where you can define exactly what the faked data should look like:: + + class UserModel + { + public function fake(Generator &$faker) + { + return [ + 'first' => $faker->firstName, + 'email' => $faker->email, + 'phone' => $faker->phoneNumber, + 'avatar' => Faker\Provider\Image::imageUrl(800, 400), + 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, + ]; + } + +Notice in this example how the first three values are equivalent to the formatters from before. However for ``avatar`` +we have requested an image size other than the default and ``login`` uses a conditional based on app configuration, +neither of which are possible using the ``$formatters`` parameter. +You may want to keep your test data separate from your production models, so it is a good practice to define +a child class in your test support folder:: + + namespace Tests\Support\Models; + + class UserFabricator extends \App\Models\UserModel + { + public function fake(&$faker) + { + +Localization +============ + +Faker supports a lot of different locales. Check their documentation to determine which providers +support your locale. Specify a locale in the third parameter while initiating a fabricator:: + + $fabricator = new Fabricator(UserModel::class, null, 'fr_FR'); + +If no locale is specified it will use the one defined in **app/Config/App.php** as ``defaultLocale``. +You can check the locale of an existing fabricator using its ``getLocale()`` method. + +Faking the Data +=============== + +Once you have a properly-initialized fabricator it is easy to generate test data with the ``make()`` command:: + + $fabricator = new Fabricator(UserFabricator::class); + $testUser = $fabricator->make(); + print_r($testUser); + +You might get back something like this:: + + array( + 'first' => "Maynard", + 'email' => "king.alford@example.org", + 'phone' => "201-886-0269 x3767", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + ) + +You can also get a lot of them back by supplying a count:: + + $users = $fabricator->make(10); + +The return type of ``make()`` mimics what is defined in the representative model, but you can +force a type using the methods directly:: + + $userArray = $fabricator->makeArray(); + $userObject = $fabricator->makeObject(); + $userEntity = $fabricator->makeObject('App\Entities\User'); + +The return from ``make()`` is ready to be used in tests or inserted into the database. Alternatively +``Fabricator`` includes the ``create()`` command to insert it for you, and return the result. Due +to model callbacks, database formatting, and special keys like primary and timestamps the return +from ``create()`` can differ from ``make()``. You might get back something like this:: + + array( + 'id' => 1, + 'first' => "Rachel", + 'email' => "bradley72@gmail.com", + 'phone' => "741-241-2356", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + 'created_at' => "2020-05-08 14:52:10", + 'updated_at' => "2020-05-08 14:52:10", + ) + +Similar to ``make()`` you can supply a count to insert and return an array of objects:: + + $users = $fabricator->create(100); + +Finally, there may be times you want to test with the full database object but you are not actually +using a database. ``create()`` takes a second parameter to allowing mocking the object, returning +the object with extra database fields above without actually touching the database:: + + $user = $fabricator(null, true); + + $this->assertIsNumeric($user->id); + $this->dontSeeInDatabase('user', ['id' => $user->id]); diff --git a/user_guide_src/source/testing/index.rst b/user_guide_src/source/testing/index.rst index d582bbaa9920..74d6239a292d 100644 --- a/user_guide_src/source/testing/index.rst +++ b/user_guide_src/source/testing/index.rst @@ -10,6 +10,7 @@ The following sections should get you quickly testing your applications. Getting Started Database + Generating Data Controller Testing HTTP Testing benchmark From 5265f3c60578eb375bdaea81f7ae18de9bef28c8 Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 8 May 2020 20:15:23 +0000 Subject: [PATCH 18/25] Correct the spacing --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e0b6a18e0767..13bd3db1f974 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ }, "require-dev": { "codeigniter4/codeigniter4-standard": "^1.0", - "fzaninotto/faker": "^1.9@dev", + "fzaninotto/faker": "^1.9@dev", "mikey179/vfsstream": "1.6.*", "phpunit/phpunit": "^8.5", "squizlabs/php_codesniffer": "^3.3" From 96353b4d379d4c3c541d9bdcd3c08e0ca65bbd9c Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 15 May 2020 16:00:05 +0000 Subject: [PATCH 19/25] Add info on overriding --- user_guide_src/source/testing/fabricator.rst | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst index ec51f2fe590b..0ba5494ee50b 100644 --- a/user_guide_src/source/testing/fabricator.rst +++ b/user_guide_src/source/testing/fabricator.rst @@ -143,3 +143,56 @@ the object with extra database fields above without actually touching the databa $this->assertIsNumeric($user->id); $this->dontSeeInDatabase('user', ['id' => $user->id]); + +Specifying Test Data +==================== + +Generated data is great, but sometimes you may want to supply a specific field for a test without +compromising your formatters configuration. Rather then creating a new fabricator for each variant +you can use ``setOverrides()`` to specify the value for any fields:: + + $fabricator->setOverrides(['first' => 'Bobby']); + $bobbyUser = $fabricator->make(); + +Now any data generated with ``make()`` or ``create()`` will always use "Bobby" for the ``first`` field: + + array( + 'first' => "Bobby", + 'email' => "latta.kindel@company.org", + 'phone' => "251-806-2169", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + ) + + array( + 'first' => "Bobby", + 'email' => "melissa.strike@fabricon.us", + 'phone' => "525-214-2656 x23546", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + ) + +``setOverrides()`` can take a second parameter to indicate whether this should be a persistent +override (default) or only for a single action:: + + $fabricator->setOverrides(['first' => 'Bobby'], false); + $bobbyUser = $fabricator->make(); + $bobbyUser = $fabricator->make(); + +Notice after the first return the fabricator stops using the overrides:: + + array( + 'first' => "Bobby", + 'email' => "belingadon142@example.org", + 'phone' => "741-857-1933 x1351", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + ) + + array( + 'first' => "Hans", + 'email' => "hoppifur@metraxalon.com", + 'phone' => "487-235-7006", + 'avatar' => "http://lorempixel.com/800/400/", + 'login' => null, + ) From 5a197b77bff4687cc05fd4a59300cdf3b8cf793d Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 15 May 2020 16:01:00 +0000 Subject: [PATCH 20/25] Make deleted field value conditionally random --- system/Test/Fabricator.php | 13 +++++++++++-- tests/system/Test/FabricatorTest.php | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 4689e50223e1..83ea47cc6c88 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -517,7 +517,8 @@ protected function createMock(int $count = null) case 'date': $datetime = date('Y-m-d'); default: - $datetime = time(); } + $datetime = time(); + } // Determine which fields we will need $fields = []; @@ -530,7 +531,15 @@ protected function createMock(int $count = null) if ($this->model->useSoftDeletes) { - $fields[$this->model->deletedField] = $datetime; + // Make a 20% chance of returning a deleted item + if (rand(1, 5) === 3) + { + $fields[$this->model->deletedField] = $datetime; + } + else + { + $fields[$this->model->deletedField] = null; + } } // Iterate over new entities and add the necessary fields diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index effdf6e0c55a..74cc1d90d9ab 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -382,6 +382,8 @@ public function testCreateMockSetsDatabaseFields() $this->assertIsInt($result->id); $this->assertIsInt($result->created_at); - $this->assertIsInt($result->deleted_at); + $this->assertIsInt($result->updated_at); + + $this->assertObjectHasAttribute('deleted_at', $result); } } From 4658b613d48e19cab601356e71c4ca4517cd9c68 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 16 May 2020 16:07:07 +0000 Subject: [PATCH 21/25] Change deleted value to null --- system/Test/Fabricator.php | 10 +--------- tests/system/Test/FabricatorTest.php | 1 + 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 83ea47cc6c88..49166f76ad4f 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -531,15 +531,7 @@ protected function createMock(int $count = null) if ($this->model->useSoftDeletes) { - // Make a 20% chance of returning a deleted item - if (rand(1, 5) === 3) - { - $fields[$this->model->deletedField] = $datetime; - } - else - { - $fields[$this->model->deletedField] = null; - } + $fields[$this->model->deletedField] = null; } // Iterate over new entities and add the necessary fields diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 74cc1d90d9ab..516a4b7a5f83 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -385,5 +385,6 @@ public function testCreateMockSetsDatabaseFields() $this->assertIsInt($result->updated_at); $this->assertObjectHasAttribute('deleted_at', $result); + $this->assertNull($result->deleted_at); } } From 0b2af265a0d5c6d3e31bf736762c5c0184b11925 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 16 May 2020 16:10:30 +0000 Subject: [PATCH 22/25] [ci skip] Clarify persistence with setOverrides --- user_guide_src/source/testing/fabricator.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst index 0ba5494ee50b..a08fc2a8d071 100644 --- a/user_guide_src/source/testing/fabricator.rst +++ b/user_guide_src/source/testing/fabricator.rst @@ -173,9 +173,9 @@ Now any data generated with ``make()`` or ``create()`` will always use "Bobby" f ) ``setOverrides()`` can take a second parameter to indicate whether this should be a persistent -override (default) or only for a single action:: +override or only for a single action:: - $fabricator->setOverrides(['first' => 'Bobby'], false); + $fabricator->setOverrides(['first' => 'Bobby'], $persist = false); $bobbyUser = $fabricator->make(); $bobbyUser = $fabricator->make(); @@ -196,3 +196,5 @@ Notice after the first return the fabricator stops using the overrides:: 'avatar' => "http://lorempixel.com/800/400/", 'login' => null, ) + +If no second parameter is supplied then passed values will persist by default. From c9bc7183098d8a0f8fc3a913813d513709f75f8e Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 16 May 2020 16:18:39 +0000 Subject: [PATCH 23/25] Use fresh model with each fabricator, with test --- system/Test/Fabricator.php | 3 ++- tests/system/Test/FabricatorTest.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 49166f76ad4f..69df7cd2f15a 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -114,7 +114,8 @@ public function __construct($model, array $formatters = null, string $locale = n { if (is_string($model)) { - $model = model($model); + // Create a new model instance + $model = model($model, false); } $this->model = $model; diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 516a4b7a5f83..9ad3517414be 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -1,5 +1,6 @@ assertInstanceOf(UserModel::class, $fabricator->getModel()); + } + public function testGetModelReturnsModel() { $fabricator = new Fabricator(UserModel::class); From 6ad992f7bb7743578ab0a56b8d858c291d07f4e1 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 16 May 2020 16:24:33 +0000 Subject: [PATCH 24/25] Succumb to Lonnie's attentiveness --- system/Test/Fabricator.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 69df7cd2f15a..05c1d1b11e49 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -90,7 +90,7 @@ class Fabricator * * @var array|null */ - protected $tmpOverrides; + protected $tempOverrides; /** * Default formatter to use when nothing is detected @@ -145,7 +145,7 @@ public function reset(): self { $this->setFormatters(); - $this->overrides = $this->tmpOverrides = []; + $this->overrides = $this->tempOverrides = []; $this->locale = config('App')->defaultLocale; $this->faker = Factory::create($this->locale); @@ -187,15 +187,15 @@ public function getFaker(): Generator //-------------------------------------------------------------------- /** - * Return and reset tmpOverrides + * Return and reset tempOverrides * * @return array */ public function getOverrides(): array { - $overrides = $this->tmpOverrides ?? $this->overrides; + $overrides = $this->tempOverrides ?? $this->overrides; - $this->tmpOverrides = $this->overrides; + $this->tempOverrides = $this->overrides; return $overrides; } @@ -215,7 +215,7 @@ public function setOverrides(array $overrides = [], $persist = true): self $this->overrides = $overrides; } - $this->tmpOverrides = $overrides; + $this->tempOverrides = $overrides; return $this; } From e581bda80dbce8c72bd0c05490ce9bf501bceffa Mon Sep 17 00:00:00 2001 From: MGatner Date: Sun, 17 May 2020 15:37:53 +0000 Subject: [PATCH 25/25] Circumvent long country name test failures --- tests/system/Database/Live/FabricatorLiveTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/system/Database/Live/FabricatorLiveTest.php b/tests/system/Database/Live/FabricatorLiveTest.php index a1ac780b4eb7..7f68bd439a98 100644 --- a/tests/system/Database/Live/FabricatorLiveTest.php +++ b/tests/system/Database/Live/FabricatorLiveTest.php @@ -26,6 +26,9 @@ public function testCreateAddsCountToDatabase() $fabricator = new Fabricator(UserModel::class); + // Some countries violate the 40 character limit so override that + $fabricator->setOverrides(['country' => 'France']); + $result = $fabricator->create($count); $this->seeNumRecords($count, 'user', []);