Introduction
If you work with Laravel at a daily basis (specially if you apply TDD), you might have figured out that during the 'test writing phase', sometimes we face situations that requires us to write custom array structures to fill our models, or some kind of data that cannot be easily faked and had to be written manually inside the Factory class.
Recently I've dealt with EditorJS and had to save the JSON data it provides in my database, so I ended up having to fake some of the data in order to easily make some assertions during testing.
For the following examples, I have a users
table with a JSON column called custom_data
.
The Problem
Many of us end up doing the obvious approach:
$user = User::factory()
->create([
'custom_data' => [
'id' => uniqid(),
'data' => [
'first_index' => 'first_value',
'second_index' => 'second_value',
'third_index' => 'third_value',
],
'type' => 'very_complex',
],
]);
The output of $user->custom_data
of course is:
array:3 [
"id" => "666251e06b701"
"data" => array:3 [
"first_index" => "first_value"
"second_index" => "second_value"
"third_index" => "third_value"
]
"type" => "very_complex"
] // app/Console/Commands/Playground.php:29
Or maybe some of us will take a more elegant, but yet sometimes problematic, solution:
$user = User::factory()
->withCustomData()
->create();
In UserFactory.php
:
public function withCustomData(): self
{
return $this->state([
'custom_data' => [
'id' => uniqid(),
'data' => [
'first_index' => 'first_value',
'second_index' => 'second_value',
'third_index' => 'third_value',
],
'type' => 'very_complex',
],
]);
}
And as you may expect, the output is the same as above.
At first glance these examples may seem good enough - at the end of the day, they work - but when we consider that this array can be different sometimes, it can quickly become a cumbersome to maintain. For instance, what if the "type" index has a different value and your application behavior depends on that? Will you pass a different parameter every time? Or even create another factory state, thus making the code larger and larger?
I mean, there is no problem to define factory states, but the thing here is to "separate responsibilities" and make our code smaller and more readable.
The issue with this approach is that the UserFactory has the responsibility of taking care of which types of data (and all data variations) it actually supports, when in reality the users table doesn't care about it. The JSON column accepts anything that is valid, regardless of what it is.
The Solution
This said, we can agree that nothing better than having a place to generate these exact data variations and complex things with a few lines of code.
For this, we are going to extend the Faker library by creating a custom provider, so we will be able to call:
$this->faker->complexArray()
or fake()->complexArray()
And that will get the job done.
In order to accomplish that, I have written a class inside Tests/Faker/Providers
namespace, named CustomComplexArrayProvider.php
The aim here is to shelter all that logic regarding this specific array and all things that may differ inside it.
In tests/Faker/Providers
:
<?php
namespace Tests\Faker\Providers;
use Faker\Provider\Base;
class CustomComplexArrayProvider extends Base
{
public function complexArray(): array
{
return [
'id' => uniqid(),
'data' => [
'first_index' => 'first_value',
'second_index' => 'second_value',
'third_index' => 'third_value',
],
'type' => 'complex',
];
}
}
As you can see, we have the complexArray
method definition and it returns a multidimensional array with 3 indexes. Of course this is a very simple and straightforward example. You can receive parameters inside this method and make it suit your testing needs.
You can even create a method for each type of data, just to make the code more readable:
In CustomComplexArrayProvider.php
:
public function notTooComplexArray(): array
{
return [
'id' => uniqid(),
'data' => [
'first_index' => 'first_value',
],
'type' => 'not_too_complex',
];
}
Okay, fair enough, but how do we use this?
To access these guys and use them within your tests, we must tell Laravel to add this data Provider in the Faker/Generator
class.
For that, I've written a Service Provider called TestServiceProvider
.
You can generate Provider classes with
php artisan make:provider
<?php
namespace App\Providers;
use Faker\Factory;
use Faker\Generator;
use Illuminate\Support\ServiceProvider;
use Tests\Faker\Providers\CustomComplexArrayProvider;
class TestServiceProvider extends ServiceProvider
{
public function register(): void
{
if (!$this->app->environment(['local', 'testing'])) {
return;
}
$this->app->singleton(
abstract: Generator::class,
concrete: function (): Generator {
$factory = Factory::create();
$factory->addProvider(new CustomComplexArrayProvider($factory));
return $factory;
}
);
$this->app->bind(
abstract: Generator::class . ':' . config('app.faker_locale'),
concrete: Generator::class
);
}
}
Then we have to register the Provider in our application, in bootstrap/providers.php
:
return [
App\Providers\AppServiceProvider::class,
App\Providers\TestServiceProvider::class,
];
As you can see, we registered a binding in the Service Container telling Laravel that it must add our custom class as a Faker provider, so now Faker is aware of the methods we've just created.
And of course, this is intended to be applied only in local/testing environments, so there's a check at the beginning of the code just to avoid this code to run in the production environment.
The singleton
call aims to allow us to use our custom class when calling them from a Faker/Generator
object, and the bind
does the same thing, but when calling from the fake()
helper.
This way, the result is:
dd(fake()->complexArray());
Output:
array:3 [
"id" => "6662549ca6be9"
"data" => array:3 [
"first_index" => "first_value"
"second_index" => "second_value"
"third_index" => "third_value"
]
"type" => "complex"
] // app/Console/Commands/Playground.php:37
Now you can use this method inside your factories, or even combine them with factory states. That's up to you now.
I hope this article helps you to make your code even easier to understand in 6 months from now 🙌
Thanks for reading!
Top comments (0)