diff --git a/composer.json b/composer.json
index a8045d379f79..13bd3db1f974 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/system/Language/en/Fabricator.php b/system/Language/en/Fabricator.php
new file mode 100644
index 000000000000..a3efbde2a114
--- /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..05c1d1b11e49
--- /dev/null
+++ b/system/Test/Fabricator.php
@@ -0,0 +1,563 @@
+ formatter
+ * @param string|null $locale Locale for Faker provider
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($model, array $formatters = null, string $locale = null)
+ {
+ if (is_string($model))
+ {
+ // Create a new model instance
+ $model = model($model, false);
+ }
+
+ $this->model = $model;
+
+ // If no locale was specified then use the App default
+ if (is_null($locale))
+ {
+ $locale = config('App')->defaultLocale;
+ }
+
+ // 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);
+ }
+
+ /**
+ * Reset state to defaults
+ *
+ * @return $this
+ */
+ public function reset(): self
+ {
+ $this->setFormatters();
+
+ $this->overrides = $this->tempOverrides = [];
+ $this->locale = config('App')->defaultLocale;
+ $this->faker = Factory::create($this->locale);
+
+ return $this;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Returns the model instance
+ *
+ * @return object Framework or compatible model
+ */
+ public function getModel()
+ {
+ 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(): Generator
+ {
+ return $this->faker;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Return and reset tempOverrides
+ *
+ * @return array
+ */
+ public function getOverrides(): array
+ {
+ $overrides = $this->tempOverrides ?? $this->overrides;
+
+ $this->tempOverrides = $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->tempOverrides = $overrides;
+
+ return $this;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * 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.
+ *
+ * @param array|null $formatters Array of [field => formatter], or null to detect
+ *
+ * @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;
+ }
+
+ /**
+ * 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): string
+ {
+ // 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
+ }
+
+ // Next look for known model fields
+ if (in_array($field, [$this->model->createdField, $this->model->updatedField, $this->model->deletedField]))
+ {
+ switch ($this->model->dateFormat)
+ {
+ case 'datetime':
+ return 'date';
+ break;
+
+ case 'date':
+ return 'date';
+ break;
+
+ case 'int':
+ return 'unixTime';
+ break;
+ }
+ }
+ elseif ($field === $this->model->primaryKey)
+ {
+ return 'numberBetween';
+ }
+
+ // Check some common partials
+ foreach (['email', 'name', 'title', 'text', 'date', 'url'] as $term)
+ {
+ if (stripos($field, $term) !== false)
+ {
+ return $term;
+ }
+ }
+
+ if (stripos($field, 'phone') !== false)
+ {
+ return 'phoneNumber';
+ }
+
+ // Nothing left, use the default
+ return $this->defaultFormatter;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Generate new entities with faked data
+ *
+ * @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)
+ {
+ // If a singleton was requested then go straight to it
+ if (is_null($count))
+ {
+ 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 $return;
+ }
+
+ /**
+ * Generate an array of faked data
+ *
+ * @return array An array of faked data
+ *
+ * @throws \RuntimeException
+ */
+ public function makeArray()
+ {
+ 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->faker);
+
+ // 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, $this->getOverrides());
+ }
+
+ /**
+ * Generate an object 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
+ */
+ public function makeObject(string $className = null): object
+ {
+ 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 $className)
+ {
+ // Set overrides manually
+ foreach ($this->getOverrides() as $key => $value)
+ {
+ $result->{$key} = $value;
+ }
+
+ return $result;
+ }
+ }
+
+ // Get the array values and apply them to the object
+ $array = $this->makeArray();
+ $object = new $className();
+
+ // Check for the entity method
+ if (method_exists($object, 'fill'))
+ {
+ $object->fill($array);
+ }
+ else
+ {
+ foreach ($array as $key => $value)
+ {
+ $object->{$key} = $value;
+ }
+ }
+
+ 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, bool $mock = false)
+ {
+ // Intercept mock requests
+ if ($mock)
+ {
+ return $this->createMock($count);
+ }
+
+ $ids = [];
+
+ // Iterate over new entities and insert each one, storing insert IDs
+ foreach ($this->make($count ?? 1) as $result)
+ {
+ $ids[] = $this->model->insert($result, true);
+ }
+
+ 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
+ *
+ * @return array|object An array or object (based on returnType), or an array of returnTypes
+ */
+ protected function createMock(int $count = null)
+ {
+ 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 = [];
+
+ if ($this->model->useTimestamps)
+ {
+ $fields[$this->model->createdField] = $datetime;
+ $fields[$this->model->updatedField] = $datetime;
+ }
+
+ if ($this->model->useSoftDeletes)
+ {
+ $fields[$this->model->deletedField] = null;
+ }
+
+ // Iterate over new entities and add the necessary fields
+ $return = [];
+ foreach ($this->make($count ?? 1) 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;
+ }
+}
diff --git a/tests/_support/Models/FabricatorModel.php b/tests/_support/Models/FabricatorModel.php
new file mode 100644
index 000000000000..de99daece4b2
--- /dev/null
+++ b/tests/_support/Models/FabricatorModel.php
@@ -0,0 +1,31 @@
+ $faker->ipv4,
+ 'description' => $faker->words(10),
+ ];
+ }
+}
diff --git a/tests/system/Database/Live/FabricatorLiveTest.php b/tests/system/Database/Live/FabricatorLiveTest.php
new file mode 100644
index 000000000000..7f68bd439a98
--- /dev/null
+++ b/tests/system/Database/Live/FabricatorLiveTest.php
@@ -0,0 +1,37 @@
+create();
+
+ $this->seeInDatabase('user', ['name' => $result->name]);
+ }
+
+ public function testCreateAddsCountToDatabase()
+ {
+ $count = 10;
+
+ $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', []);
+ }
+
+}
diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php
new file mode 100644
index 000000000000..9ad3517414be
--- /dev/null
+++ b/tests/system/Test/FabricatorTest.php
@@ -0,0 +1,401 @@
+ 'name',
+ 'email' => 'email',
+ 'country' => 'country',
+ 'deleted_at' => 'date',
+ ];
+
+ //--------------------------------------------------------------------
+
+ public function testConstructorWithString()
+ {
+ $fabricator = new Fabricator(UserModel::class);
+
+ $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 testModelUsesNewInstance()
+ {
+ // Inject the wrong model for UserModel to show it is ignored by Fabricator
+ $mock = new FabricatorModel();
+ ModelFactory::injectMock('Tests\Support\Models\UserModel', $mock);
+
+ $fabricator = new Fabricator(UserModel::class);
+ $this->assertInstanceOf(UserModel::class, $fabricator->getModel());
+ }
+
+ 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());
+ }
+
+ //--------------------------------------------------------------------
+
+ 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);
+
+ $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('date', $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);
+
+ $result = $fabricator->makeArray();
+
+ $this->assertIsArray($result);
+ }
+
+ public function testMakeArrayUsesOverrides()
+ {
+ $overrides = ['name' => 'The Admiral'];
+
+ $fabricator = new Fabricator(UserModel::class, $this->formatters);
+ $fabricator->setOverrides($overrides);
+
+ $result = $fabricator->makeArray();
+
+ $this->assertEquals($overrides['name'], $result['name']);
+ }
+
+ public function testMakeArrayReturnsValidData()
+ {
+ $fabricator = new Fabricator(UserModel::class, $this->formatters);
+
+ $result = $fabricator->makeArray();
+
+ $this->assertEquals($result['email'], filter_var($result['email'], FILTER_VALIDATE_EMAIL));
+ }
+
+ public function testMakeArrayUsesFakeMethod()
+ {
+ $fabricator = new Fabricator(FabricatorModel::class);
+
+ $result = $fabricator->makeArray();
+
+ $this->assertEquals($result['name'], filter_var($result['name'], FILTER_VALIDATE_IP));
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testMakeObjectReturnsModelReturnType()
+ {
+ $fabricator = new Fabricator(EntityModel::class);
+ $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);
+
+ $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));
+ }
+
+ //--------------------------------------------------------------------
+
+ 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);
+ }
+
+ //--------------------------------------------------------------------
+
+ 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->updated_at);
+
+ $this->assertObjectHasAttribute('deleted_at', $result);
+ $this->assertNull($result->deleted_at);
+ }
+}
diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst
new file mode 100644
index 000000000000..a08fc2a8d071
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator.rst
@@ -0,0 +1,200 @@
+####################
+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]);
+
+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 or only for a single action::
+
+ $fabricator->setOverrides(['first' => 'Bobby'], $persist = 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,
+ )
+
+If no second parameter is supplied then passed values will persist by default.
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