laravelmastery
SnippetsArticlesSeriesNews

Test-driven Laravel · Lesson 2 of 3

Test-driven Laravel · Lesson 2

Validation errors

The paths that matterTesting

The happy path stored the invoice the action promised. A missing number should not create an invoice. The test posts the bad input and asserts that the response names the fields. It also asserts that the table did not gain a row.

Video

01 Assert the fields, not the sentence

assertInvalid() checks the session errors. The wording of the message can change without the rule changing, so the test names the field.

PHP
it('requires a number and an issue date', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/invoices', [])
        ->assertInvalid(['number', 'issued_on']);

    $this->assertDatabaseCount('invoices', 0);
});

The form request article is where those rules live, including authorize() and prepareForValidation(). This test does not call the form request class. The HTTP response is the behaviour.

Form requests are doing more than you think.

One test for the empty request is enough when the rules are simple. A status that must be one of three values is a dataset, in Datasets, not three copies of this test.

02 A bad date is still a validation failure

An issue date in the wrong shape should fail the same way an empty one does. The test names issued_on, and the count stays zero. The message text stays out of the assertion.

PHP
it('rejects an issue date that is not a date', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/invoices', [
            'number' => 'INV-1042',
            'issued_on' => 'next tuesday',
        ])
        ->assertInvalid(['issued_on'])
        ->assertValid(['number']);

    $this->assertDatabaseCount('invoices', 0);
});

assertValid() is the pair of assertInvalid(). The number was present and well formed, so a failure on number would mean the rule rejected a field this test was not trying to break. The session still redirects back. The invoice table does not gain a row either way.

assertSessionHasErrors() is the older name for the same session bag. assertInvalid() is the one to reach for. Both ignore the sentence. If the product copies the message onto the invoice page, assertSee() can check that sentence in a view test. It should not replace the field name here, or a copy edit fails the rule.

Recap

You can assert the fields that failed and that no invoice was stored.

  • assertInvalid() names fields, not the sentence.
  • assertValid() is how you say the number was fine when the date was not.
  • A closed set of bad statuses is a dataset, not three copies of this test.

Check yourself · 3 questions

Question 1 of 3The message text changes and the rule does not. A test that asserts the sentence fails. What should it assert?

A bad number stores nothing and names the field. Next, the team that must not see the invoice.

← PreviousHTTP tests for happy paths

On this page

  • Assert the fields, not the sentence
  • A bad date is still a validation failure
  • 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.