Using Laravel Model Factories

Published on : July 30,2022
Using Laravel Model Factories

Laravel model factories are one of the excellent functions you can use for your application with regards to checking out. They provide a manner to define facts that is predictable and smooth to duplicate so that your assessments are steady and managed.

Permit's begin with a easy example. We have an software used for blogging, so certainly, we've got a submit version that has a standing for if the post is published, drafted, or queued. Allows take a look at the Eloquent version for this example:

declare(strict_types=1);
 
namespace App\Models;
 
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Model;
 
class Post extends Model
{
	protected $fillable = [
		'title',
		'slug',
		'content',
		'status',
		'published_at',
	];
 
	protected $casts = [
		'status' => PostStatus::class,
		'published_at' => 'datetime',
	];
}

 

As you can see here, we have an Enum for the fame column, which we will design now. The usage of an enum here permits us to take advantage of personal home page PHP 8.1 capabilities rather than undeniable strings, Boolean flags, or messy database enums.

declare(strict_types=1);
 
namespace App\Publishing\Enums;
 
enum PostStatus: string
{
	case PUBLISHED = 'published';
	case DRAFT = 'draft';
	case QUEUED = 'queued';
}

 

Now, let's get returned to the subject we're here to discuss: model factories. A simple factory would look very simple:

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
	protected $model = Post::class;
 
	public function definition(): array
	{
		$title = $this->faker->sentence();
		$status = Arr::random(PostStatus::cases());
 
		return [
			'title' => $title,
			'slug' => Str::slug($title),
			'content' => $this->faker->paragraph(),
			'status' => $status->value,
			'published_at' => $status === PostStatus::PUBLISHED
				? now()
				: null,
		];
	}
}

 

So in our assessments, we will now fast name our publish factory to create a submit for us. Let's see how we might do this:

it('can update a post', function () {
	$post = Post::factory()->create();
 
	putJson(
		route('api.posts.update', $post->slug),
		['content' => 'test content',
	)->assertSuccessful();
 
	expect(
		$post->refresh()
	)->content->toEqual('test content');
});

 

A simple sufficient take a look at, but what occurs if we've got enterprise guidelines that say you can only update unique columns depending on put up kind? Let's refactor our test to make sure we can do that:

it('can update a post', function () {
	$post = Post::factory()->create([
		'type' => PostStatus::DRAFT->value,
	]);
 
	putJson(
		route('api.posts.update', $post->slug),
		['content' => 'test content',
	)->assertSuccessful();
 
	expect(
		$post->refresh()
	)->content->toEqual('test content');
});

 

Ideal, we will skip an argument into the create method to ensure that we are setting the ideal type whilst we create it so that our commercial enterprise regulations aren't going to complain. But that may be a little cumbersome to hold having to write, so allows refactor our factory a little to add techniques to modify the kingdom:

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
	protected $model = Post::class;
 
	public function definition(): array
	{
		$title = $this->faker->sentence();
 
		return [
			'title' => $title,
			'slug' => Str::slug($title),
			'content' => $this->faker->paragraph(),
			'status' => PostStatus::DRAFT->value,
			'published_at' => null,
		];
	}
 
	public function published(): static
	{
		return $this->state(
			fn (array $attributes): array => [
				'status' => PostStatus::PUBLISHED->value,
				'published_at' => now(),
			],
		);
	}
}

 

We set a default for our manufacturing facility so that all newly created posts are drafts. Then we add a method for putting the kingdom to be posted, a good way to use the perfect Enum cost and set the posted date - loads greater predictable and repeatable in a testing environment. Let's see what our take a look at could now appear like:

it('can update a post', function () {
	$post = Post::factory()->create();
 
	putJson(
		route('api.posts.update', $post->slug),
		['content' => 'test content',
	)->assertSuccessful();
 
	expect(
		$post->refresh()
	)->content->toEqual('test content');
});

 

Back to being a simple test - so if we have multiple tests that want to create a draft post, they can use the factory. Now let us write a test for the published state and see if we get an error.

it('returns an error when trying to update a published post', function () {
	$post = Post::factory()->published()->create();
 
	putJson(
		route('api.posts.update', $post->slug),
		['content' => 'test content',
	)->assertStatus(Http::UNPROCESSABLE_ENTITY());
 
	expect(
		$post->refresh()
	)->content->toEqual($post->content);
});

 

This time we are testing that we are receiving a validation error status when we try to update a published post. This ensures that we protect our content and force a specific workflow in our application.

So what happens if we also want to ensure specific content in our factory? We can add another method to modify the state as we need to:

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
	protected $model = Post::class;
 
	public function definition(): array
	{
		return [
			'title' => $title = $this->faker->sentence(),
			'slug' => Str::slug($title),
			'content' => $this->faker->paragraph(),
			'status' => PostStatus::DRAFT->value,
			'published_at' => null,
		];
	}
 
	public function published(): static
	{
		return $this->state(
			fn (array $attributes): array => [
				'status' => PostStatus::PUBLISHED->value,
				'published_at' => now(),
			],
		);
	}
 
	public function title(string $title): static
	{
		return $this->state(
			fn (array $attributes): array => [
				'title' => $title,
				'slug' => Str::slug($title),
			],
		);
	}
}

 

So in our tests, we can create a new test that ensures that we can update a draft posts title through our API:

it('can update a draft posts title', function () {
	$post = Post::factory()->title('test')->create();
 
	putJson(
		route('api.posts.update', $post->slug),
		['title' => 'new title',
	)->assertSuccessful();
 
	expect(
		$post->refresh()
	)->title->toEqual('new title')->slug->toEqual('new-title');
});

 

So we can control things in our test environment using factory states nicely, giving us as much control as we need. Doing this will ensure that we are consistently preparing our tests or would be a good reflection of the applications state at specific points.

What do we do if we need to create many models for our tests? How can we do this? The easy answer would be to tell the factory:

it('lists all posts', function () {
	Post::factory(12)->create();
 
	getJson(
		route('api.posts.index'),
	)->assertOk()->assertJson(fn (AssertableJson $json) =>
		$json->has(12)->etc(),
	);
});

 

So we are creating 12 new posts and ensuring that when we get the index route, we have 12 posts returning. Instead of passing the count into the factory method, you can also use the count method:

Post::factory()->count(12)->create();

 

However, there are times in our application when we might want to run things in a specific order. Let's say we want the first one to be a draft, but the second is published?

it('shows the correct status for the posts', function () {
	Post::factory()
		->count(2)
		->state(new Sequence(
			['status' => PostStatus::DRAFT->value],
			['status' => PostStatus::PUBLISHED->value],
		))->create();
 
	getJson(
		route('api.posts.index'),
	)->assertOk()->assertJson(fn (AssertableJson $json) =>
		$json->where('id', 1)
			->where('status' PostStatus::DRAFT->value)
			->etc();
	)->assertJson(fn (AssertableJson $json) =>
		$json->where('id', 2)
			->where('status' PostStatus::PUBLISHED->value)
			->etc();
	);
});

Categories : Laravel PHP

Tags : Laravel 9

Praful Sangani
Praful Sangani
I'm a passionate full-stack developer with expertise in PHP, Laravel, Angular, React Js, Vue, Node, Javascript, JQuery, Codeigniter, and Bootstrap. I enjoy sharing my knowledge by writing tutorials and providing tips to others in the industry. I prioritize consistency and hard work, and I always aim to improve my skills to keep up with the latest advancements. As the owner of Open Code Solution, I'm committed to providing high-quality services to help clients achieve their business goals.


0 Comments

Leave a comment

We'll never share your email with anyone else. Required fields are marked *

Related Articles

Access specifier in php
Praful Sangani By Praful Sangani - July 20,2022