0

Using factories in tests — create(), make(), createMany()

Intermediate5 min read·lv-15-005

Concept

Factory methods behave differently depending on whether you want models persisted to the database or held purely in memory. create() calls Model::save() for each instance and all its related models, wrapping everything in a single database transaction by default. make() runs the factory's definition() and any applied states and returns a fully hydrated model instance with no database interaction whatsoever — useful for unit tests that do not need persistence. createMany() and makeMany() accept an integer count and return an Illuminate\Database\Eloquent\Collection.

In Laravel feature tests that extend Illuminate\Foundation\Testing\TestCase, you typically combine factories with the RefreshDatabase or DatabaseTransactions trait. RefreshDatabase truncates all tables and re-runs migrations at the start of each test class, giving every test a clean schema. DatabaseTransactions wraps each test in a transaction that is rolled back at the end, which is faster than re-running migrations but requires your test not to spawn processes that bypass the transaction.

createQuietly() and makeMany() are valuable advanced variants. createQuietly() suppresses all Eloquent model events (creating, created, saving, etc.) during creation, preventing side effects like audit logs, cache invalidations, or downstream queue jobs from firing. This isolates the test to the behavior under test rather than the behavior of observers. You can also call withoutEvents() on the factory for the same effect with finer control.

The state() method on the Factory instance accepts a closure or array and merges the result onto definition(). When you chain states, each state's closure receives the attributes array accumulated so far — so a later state can inspect and react to earlier states. This makes the final attribute set deterministic and predictable.

MethodDB writeReturnsUse case
create(attrs)YesModelFeature/integration tests, seeders
make(attrs)NoModelUnit tests, DTO construction
createMany([...])YesCollectionBatch seeding
makeMany(n)NoCollectionBulk in-memory setup
createQuietly(attrs)YesModelBypasses model events
raw(attrs)NoarrayTesting array-level logic

Code Example

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostApiTest extends TestCase
{
    use RefreshDatabase; // clean DB before each test

    public function test_published_posts_are_returned(): void
    {
        // create() — persists, triggers model events
        $user = User::factory()->create();

        // createMany() — three published posts
        $published = Post::factory()
            ->count(3)
            ->for($user)
            ->state(['status' => 'published'])
            ->create();

        // createQuietly() — no PostCreated event, no queue job
        $draft = Post::factory()
            ->for($user)
            ->state(['status' => 'draft'])
            ->createQuietly();

        $response = $this->actingAs($user)
            ->getJson('/api/posts?status=published');

        $response->assertOk()
            ->assertJsonCount(3, 'data');
    }

    public function test_post_dto_mapping(): void
    {
        // make() — no DB touch, pure unit test
        $post = Post::factory()->make([
            'title' => 'Test Title',
        ]);

        $dto = \App\DTOs\PostDTO::fromModel($post);

        $this->assertSame('Test Title', $dto->title);
    }

    public function test_raw_returns_attribute_array(): void
    {
        // raw() — plain PHP array, useful for testing form validation
        $attrs = Post::factory()->raw();

        $response = $this->postJson('/api/posts', $attrs);
        $response->assertCreated();
    }
}

Interview Q&A

Q: When should you use make() instead of create() in a test, and what are the trade-offs?

Use make() when the test does not require the model to exist in the database — typically in unit tests that exercise a method on the model class itself, test a DTO transformer, or verify business logic that operates on the model's attribute values. make() is significantly faster because it skips the database round-trip entirely. The trade-off is that make() models have no id and cannot participate in real relationship queries. Use create() when the test exercises code that fetches, filters, or joins models through the database, or when the system under test invokes $model->save() or relationship methods.


Q: What does createQuietly() do under the hood, and why is it useful in tests?

createQuietly() calls Model::withoutEvents(fn() => $factory->create()) internally, which temporarily removes all event listeners registered on the model class. This prevents observers, $dispatchesEvents mappings, and creating/created hooks from firing. In a test suite this is useful when you need a record to exist in the database purely as a fixture but you do not want to trigger its side effects — for instance, creating a Payment record without sending an invoice email or dispatching a PaymentReceived event that would require you to fake the event system.


Q: How does RefreshDatabase differ from DatabaseTransactions, and which should you prefer?

RefreshDatabase drops and re-creates the entire schema (using migrate:fresh) the first time the test suite runs, then wraps each test in a transaction that is rolled back — so schema setup happens once per suite, not once per test. DatabaseTransactions simply wraps each test in a transaction without touching the schema, assuming the schema is already correct. RefreshDatabase is safer (the schema is always fresh) and is the default for most Laravel projects. DatabaseTransactions is faster when you are confident the schema won't drift and want to avoid any migration overhead during the test run.