laravelmastery
SnippetsArticlesSeriesNews

Test-driven Laravel · Lesson 3 of 3

Test-driven Laravel · Lesson 3

Datasets

Edges and judgementTesting

sent_at is pinned, so the timestamp no longer flakes. Three bad statuses are one behaviour: the invoice is rejected. Copying the test three times makes three places to update when the route changes. A dataset runs the same test once for each value.

Video

01 One test, a list of bad values

The argument is the status. The name before the arrow is what Pest prints when that case fails.

PHP
it('rejects a status the invoice cannot hold', function (string $status) {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/invoices', [
            'number' => 'INV-1042',
            'issued_on' => '2026-10-22',
            'status' => $status,
        ])
        ->assertInvalid(['status']);
})->with([
    'blank' => '',
    'unknown' => 'banana',
    'paid' => 'paid',
]);

paid is in the list because a request must not set the status to paid. That happens in the job, after the provider accepts the charge. The dataset says so without a second test body.

Keep a dataset to values of the same behaviour. A guest and a bad status are different failures, and they stay as different tests.

02 A second dataset can cover the statuses that are allowed

The test above is the reject list. The accept list is a different outcome: the invoice is stored, and the status column matches the value that was posted. Same shape, different assertion, so it stays a second test rather than a flag inside the first.

PHP
it('stores a status the invoice is allowed to hold', function (string $status) {
    $user = User::factory()->create();

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

    $this->assertDatabaseHas('invoices', [
        'number' => 'INV-1042',
        'status' => $status,
    ]);
})->with([
    'draft' => 'draft',
    'issued' => 'issued',
]);

paid is missing on purpose. A request that posts paid belongs in the reject dataset. The job sets paid after the provider accepts the charge, and no row in this test should be able to do that by itself.

Pest prints the name before the arrow when a case fails, so blank, unknown and paid tell you which value broke. A list of bare strings still runs. It fails as dataset 0, and you have to open the file to see which status that was. Name the cases.

Recap

You can run one rejection test for each illegal status without copying the request.

  • The name before the arrow is what Pest prints when that case fails.
  • paid is rejected on the form because the job sets it, not the request.
  • A guest and a bad status are different failures. They stay different tests.

Check yourself · 3 questions

Question 1 of 3blank, banana, and paid are three tests with the same body. What replaces the copies?

One body covers each bad status. Next, the tests this suite deliberately leaves out.

← PreviousFreezing time

On this page

  • One test, a list of bad values
  • A second dataset can cover the statuses that are allowed
  • 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.