Back to Course |
Testing in Laravel 11: Advanced Level

Customized Factory Class

In this lesson, let's look at an interesting example of a Factory class. I found an example on the spatie/freek.dev open-source repository, where they created a separate Factory class not in the database/factories but in tests/factories, mimicking the same behavior but with much more customized functions.


How That Factory Class is Used

The usage is similar to that of a regular Factory. New class initialization, then some type identical to how factories have states and the create for creating.

database/seeders/PostSeeder.php:

use Illuminate\Database\Seeder;
use Tests\Factories\PostFactory;
 
class PostSeeder extends Seeder
{
public function run(): void
{
(new PostFactory(2))->tweet()->create();
(new PostFactory(2))->original()->create();
(new PostFactory(2))->link()->create();
}
}

It is also used in the tests.

tests/Feature/Models/PostTest.php:

use Tests\Factories\PostFactory;
 
// ...
 
it('can render a series toc and next link on post', function () {
$posts = PostFactory::series(10);
 
expect($posts->first()->refresh()->html)->toMatchSnapshot();
});
 
// ...

Custom Factory Code

So, how does this custom Factory look?

tests/Factories/PostFactory.php:

namespace Tests\Factories;
 
use App\Models\Post;
use Faker\Factory;
use Faker\Generator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
 
class PostFactory
{
private int $times;
 
private ?string $type;
 
public function __construct(int $times = 1)
{
$this->times = $times;
}
 
public function tweet()
{
$this->type = Post::TYPE_TWEET;
 
return $this;
}
 
public function original()
{
$this->type = Post::TYPE_ORIGINAL;
 
return $this;
}
 
public function link()
{
$this->type = Post::TYPE_LINK;
 
return $this;
}
 
public function create(array $attributes = [])
{
foreach (range(1, $this->times) as $i) {
/** @var \App\Models\Post $post */
$post = Post::factory()->create($attributes);
if (is_null($this->type)) {
$this->type = Arr::random([
Post::TYPE_LINK,
Post::TYPE_ORIGINAL,
Post::TYPE_TWEET,
]);
}
 
if ($this->type === Post::TYPE_LINK) {
$post->original_content = false;
$post->external_url = $this->faker()->randomElement([
'https://spatie.be',
'https://laravel.com',
'https://ohdear.app',
'https://flareapp.io',
]);
$post->title = $this->faker()->sentence;
}
 
if ($this->type === Post::TYPE_TWEET) {
$post->original_content = false;
$post->external_url = '';
$post->title = $this->faker()->sentence;
$post->attachTag('tweet');
$post->text = $this->getStub('tweet');
}
 
if ($this->type === Post::TYPE_ORIGINAL) {
$post->original_content = true;
$post->external_url = '';
$post->title = $this->faker()->sentence;
$post->text = $this->getStub('original');
$post->tweet_url = 'https://twitter.com/TwitterAPI/status/1150141056027103247';
}
 
$post->save();
}
 
if ($this->times === 1) {
return $post;
}
}
 
protected function faker(): Generator
{
return Factory::create();
}
 
protected function getStub(string $stubName): string
{
return file_get_contents(__DIR__."/stubs/{$stubName}.md");
}
 
public static function series(int $count): Collection
{
$posts = Post::factory()->count($count)->create([
'title' => 'Test post',
'original_content' => true,
'published' => true,
'series_slug' => 'test-series',
'text' => '[series-toc] This is the blog post [series-next-post]',
]);
 
foreach ($posts as $i => $post) {
$post->update(['title' => "Series title part {$i}: Lorem ipsum"]);
 
if ($i === 0) {
$firstSentence = 'On [our Laravel powered company website](https://spatie.be) we sell digital products. ';
 
$post->update(['text' => "{$firstSentence}{$post->text}"]);
}
}
 
return $posts;
}
}

First, as you can see, it doesn't extend any other class. It's just a PHP class.

Then, in the __construct(), you can set the number of records to create.

Next, there are three methods: tweet(), original(), link() to set the post type. This behavior is similar to Laravel factory states.

The most important create() method has a parameter of attributes that can overwrite the default values. Same as using typical Eloquent Factories? However, this create() method has custom Post creation logic.

Also, inside, it uses the regular PostFactory and Faker to create a post.

There is also a series() method where posts are created with different logic.

So, the overall purpose of this class is to set up a custom behavior specific to that project instead of using the default Factories from Laravel. In most cases, you wouldn't need such customization, but knowing about such a possibility is beneficial.