laravelmastery
SnippetsArticlesSeriesNews

Test-driven Laravel · Lesson 1 of 3

Test-driven Laravel · Lesson 1

Factories that read like the domain

The shape of a testTesting

The database is empty, so the test has to build the invoice. Invoice::factory()->create(['status' => 'issued', 'issued_on' => now()]) says which columns changed. It does not say why the test needs that invoice. A state gives the reason a name, and every test shares the same columns.

Video

01 Name the state the test is about

The default factory stays a normal invoice: a draft, on a team, with every required column filled. issued() is the edge the test asked for.

Example.phpPHP
class Example
{
    public function issued(): static
    {
        return $this->state(fn () => [
            'status' => 'issued',
            'issued_on' => now()->toDateString(),
        ]);
    }
}
PHP
$invoice = Invoice::factory()->issued()->create();

$this->actingAs($invoice->team->owner)
    ->post('/invoices/'.$invoice->id.'/send')
    ->assertRedirect();

The snippet on factory states shows the same idea for a void invoice, with voided().

Name the interesting states.

Leave the overrides in the test only when that one test is the reason the value exists. If a second test copies the array, it is a state.

02 Chain a second state when the test needs both

issued() sets the status and the date. forTeam() sets the team when the test already has one. Chaining reads as the invoice the test means, and the default factory still fills the columns neither state mentioned.

Example.phpPHP
class Example
{
    public function forTeam(Team $team): static
    {
        return $this->state(fn () => [
            'team_id' => $team->id,
        ]);
    }
}
PHP
$invoice = Invoice::factory()
    ->forTeam($user->currentTeam)
    ->issued()
    ->create();

A state that also creates lines, mail and a payment is doing the test’s arrange in hiding. Keep a state to the columns that define the word in its name. The test adds the lines when the lines are what that test is about.

afterCreating() is the hook for those lines, and only on the state that means “an invoice with lines”. A default factory that always creates lines makes every test pay for rows it never asserts. Name that state withLines() when a second test needs it. Until then, the test can call lines()->create() itself.

Recap

You can name issued() so a test reads as the invoice it means, and keep the default factory boring.

  • A state shared by two tests belongs in the factory. A value one test needs can stay in that test.
  • Chain states. Do not hide a second fact inside the first.
  • A state that also sends mail is the test's arrange in hiding.

Check yourself · 3 questions

Question 1 of 3Two tests copy ['status' => 'issued', 'issued_on' => now()]. What is that array?

The interesting invoice has a name. Next, what actingAs() has been doing since the first test.

← PreviousRefreshing the database

On this page

  • Name the state the test is about
  • Chain a second state when the test needs both
  • Recap
  • Check yourself

Test-driven Laravel

  • 1 Overview

The shape of a test

  • 2 What a feature test is for
  • 3 Arrange, act, assert
  • 4 Refreshing the database
  • 5 Factories that read like the domain
  • 6 Acting as a user

The paths that matter

  • 7 HTTP tests for happy paths
  • 8 Validation errors
  • 9 Authorisation

Edges and judgement

  • 10 Faking the mail
  • 11 Faking the queue
  • 12 Faking storage and HTTP
  • 13 Freezing time
  • 14 Datasets
  • 15 What not to test

Test-driven Laravel

Lessons

  1. 01Overview

The shape of a test

  1. 02What a feature test is for
  2. 03Arrange, act, assert
  3. 04Refreshing the database
  4. 05Factories that read like the domain
  5. 06Acting as a user

The paths that matter

  1. 07HTTP tests for happy paths
  2. 08Validation errors
  3. 09Authorisation

Edges and judgement

  1. 10Faking the mail
  2. 11Faking the queue
  3. 12Faking storage and HTTP
  4. 13Freezing time
  5. 14Datasets
  6. 15What not to test

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.