laravelmastery
SnippetsArticlesSeriesNews

Test-driven Laravel · Lesson 3 of 3

Test-driven Laravel · Lesson 3

What not to test

Edges and judgementTesting

The dataset ran one body against each bad status. A test that cannot fail when the feature breaks is noise. So is a test that fails when you rename a private method and nothing else changes. This series stops at the behaviour.

Video

01 Leave the framework alone

Do not assert that belongsTo() returns a relation. Do not assert the SQL. Do not call a private method through reflection. Do not copy every validation rule into a second test that instantiates the form request: the HTTP test already posted the empty form.

Assert the columns the outcome depends on. issued_on matters when the test is about the date. created_at does not, unless the test froze time in order to check it.

One behaviour per test. The happy path, the validation error, the other team, and the queued mail are four tests. A single test that does all four fails for one reason and you have to read the whole method to find which reason.

Do not hit the network, the real disk, or the database the site is using. The fakes and RefreshDatabase exist so the suite can run without them.

02 A test of the framework fails when you upgrade, not when you break the invoice

Asserting that a factory can create a model, with no further claim, is an example. Laravel already has that test. Yours should fail when issuing an invoice stops working.

PHP
it('does not need to prove the framework saves a model', function () {
    $invoice = Invoice::factory()->create(['number' => 'INV-1042']);

    expect($invoice->number)->toBe('INV-1042');
});

That test passes for as long as Eloquent and the factory work. Delete it. The HTTP test that posts INV-1042 and reads the row back already covers the number, and it fails when the controller stops writing it. The same rule retires a test that only calls $invoice->team and expects a BelongsTo instance. The authorisation test already proved the team is the one that matters.

The test worth keeping is the one that would go red when the controller stops issuing the invoice. A private method extracted from that controller does not earn a test of its own. The HTTP test still posts the same number and still reads the same row. Renaming the private method leaves the test green, which is what you want. That is the suite staying pointed at the invoice, not at the shape of the class.

PHP
it('stores the posted invoice number', 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,
    ]);
});

Recap

You can delete a test that only proves the framework still saves a model.

  • Assert the columns the outcome depends on.
  • Do not assert SQL, belongsTo(), or a private method.
  • The HTTP test that posts INV-1042 already covers the number.

Check yourself · 3 questions

Question 1 of 3A test creates an invoice with a factory and expects the number back, and never sends a request. When does it fail?

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.

← PreviousDatasets

Back to the overview

On this page

  • Leave the framework alone
  • A test of the framework fails when you upgrade, not when you break the invoice
  • 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.