Home  >  Article  >  PHP Framework  >  How to use model factory in Laravel application?

How to use model factory in Laravel application?

青灯夜游
青灯夜游forward
2022-11-28 20:26:221156browse

How to use model factory in Laravel application? The following article will introduce to you how to use Laravel model engineering in testing. I hope it will be helpful to you!

How to use model factory in Laravel application?

#Laravel Model Factory is one of the best features you can use while testing in your application. They provide a way to define data that is predictable and easily replicable so that your tests remain consistent and controllable.

Let's start with a simple example. We have an application for blogging, so naturally we have a Post model that has a status of Published, Drafted, or Queued. Let's take a look at the Eloquent model 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 status column, which we will now design. Using enums here allows us to take advantage of PHP 8.1 features instead of plain strings, boolean flags, or confusing 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 back to the topic we’re discussing here: Model Factory. A simple factory looks 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 tests we can now quickly call our post factory to create a post for us. Let's see how we can 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 enough test, but what happens if our business rules state that you can only update specific columns based on post type? Let's refactor our test to make sure we can do this:

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');
});

Perfectly, we can pass a parameter to the create method to make sure we set the correct type when we create it, so that our The business rules won't complain. But writing it like this is a bit cumbersome, so let's refactor our factory a little and add a method to modify the status:

 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 value for the factory so that all newly created posts are drafts. We then add a method to set the state to publish, which will use the correct Enum value and set the publish date - more predictable and repeatable in a test environment. Let's see what our test looks like now:

 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 a simple test - so if we have multiple tests that want to create a draft post, they can use a factory. Now let's write a test for the published state to see if there are any errors.

 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 whether we receive a validation error status when we try to update a published post. This ensures we protect our content and enforce specific workflows within our applications.

So what happens if we also want to ensure specific content in the factory? We can add another method to modify the status if needed:

 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 within our tests we can create a new test to make sure we can update the draft post title via 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 use factory state nicely to control things in our test environment, giving us as much control as possible. Doing so will ensure that we are consistently prepared for testing, or that the state of the application at a specific point is well reflected.

What should we do if we need to create many models for our tests? What should we do? The simple answer is 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 making sure that when we get the index route, we have 12 posts returned. Instead of passing count to the factory method, you can also use the count method:

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

However, sometimes in our application we may want to run things in a specific order. Suppose we want the first one to be a draft, but the second one to be 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();
    );
});

How do you use model factories in your application? Have you found any cool ways to use them? Tell us on twitter!

Original address: https://laravel-news.com/laravel-model-factories

Translation address: https://learnku.com/laravel/t/70290

[Related recommendations: laravel video tutorial]

The above is the detailed content of How to use model factory in Laravel application?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:learnku.com. If there is any infringement, please contact admin@php.cn delete