laravelmastery
SnippetsArticlesSeriesNews

All series

Series · Overview

Test-driven Laravel

15 lessons2 hr 40 minAll levelsFreeTesting

Feature tests, fakes and factories that give you the confidence to refactor without fear.

By the end, issuing an invoice is covered by a suite that fails when the feature breaks, and stays green when you refactor.

Video

This series builds the tests for issuing an invoice. A feature test sends a request and asserts what a person would notice: a redirect, a saved row, a forbidden response. The framework’s own internals stay out of the assertion.

Each lesson is a page. Mark it as read when you have tried the idea, then use Next. The last lesson points at the next series, and this overview stays one link away.

PHP
it('issues an invoice for the signed-in team', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/invoices', [
            'number' => 'INV-1042',
            'issued_on' => '2026-10-22',
        ])
        ->assertRedirect();

    $this->assertDatabaseHas('invoices', [
        'number' => 'INV-1042',
        'team_id' => $user->current_team_id,
    ]);
});

The arrange is one user. The act is the post. The assert is the row on that user’s team. The lessons below take that shape apart: the database, the factory, the fakes, and the tests that are not worth writing.

01 The lessons

  • What a feature test is for. Check one thing a person can do, through the same HTTP door they use.
  • Arrange, act, assert. Set the world up, do one thing, then say what changed.
  • Refreshing the database. Give each test an empty database, and keep that database off the one the site is serving.
  • Factories that read like the domain. Name the interesting invoice, instead of repeating a column array in every test.
  • Acting as a user. Sign a team member in without going through the login form.
  • HTTP tests for happy paths. Follow the redirect, then assert the row the action promised.
  • Validation errors. Name the fields that failed, and assert that no invoice was stored.
  • Authorisation. Expect 403 when the signed-in team does not own the invoice.
  • Faking the mail. Assert the mailable was queued, without sending it.
  • Faking the queue. Assert the job was pushed, and leave handle() unrun.
  • Faking storage and HTTP. Keep the PDF off the real disk, and fail the test if the code calls an unexpected host.
  • Freezing time. Pin now() so a timestamp assertion does not depend on the clock.
  • Datasets. Run one test body against each bad status.
  • What not to test. Leave the framework, the SQL, and the private methods alone.

Part 1 of 3 · The shape of a test

What a feature test is for, arrange-act-assert, refreshing the database, factories, acting as a user.

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

Part 2 of 3 · The paths that matter

Happy paths, validation errors, authorisation.

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

Part 3 of 3 · Edges and judgement

Faking mail, faking the queue, faking storage and HTTP, freezing time, datasets, what not to test.

  1. Faking the mail
  2. Faking the queue
  3. Faking storage and HTTP
  4. Freezing time
  5. Datasets
  6. What not to test

Where the series leaves you

The finished suite for issuing an invoice:

  • tests/Feature/IssueInvoiceTest.php issues one for the signed-in team.
  • tests/Feature/GuestInvoiceTest.php sends a guest to login.
  • tests/Feature/InvoiceValidationTest.php rejects an empty number and stores nothing.
  • tests/Feature/InvoiceAuthorisationTest.php returns 403 for another team.
  • tests/Feature/InvoiceOwnerTest.php lets the owner through.
  • tests/Feature/InvoiceMailTest.php asserts the mailable was queued.
  • tests/Feature/InvoicePaymentTest.php asserts the payment job was pushed.
  • tests/Feature/InvoicePdfTest.php asserts the PDF was stored.
  • tests/Feature/InvoiceTimestampTest.php pins sent_at.
  • tests/Feature/InvoiceStatusTest.php runs each bad status from one dataset.

The framework, the SQL, and the private methods are deliberately not in that list.

On this page

  • The lessons
  • The shape of a test
  • The paths that matter
  • Edges and judgement
  • Where the series leaves you

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.