Introduction
Laravel gives you an expressive set of assertions for every layer of an HTTP response and every row in your test database. Mastering a handful of them is enough to cover the vast majority of feature tests.
Key Concepts
- Response assertions: Methods on the
TestResponseobject returned byget(),post(), etc. - Database assertions: Global helpers that query your test database directly —
assertDatabaseHas,assertDatabaseMissing,assertDatabaseCount. - Chainable API: All response assertions return
$this, so you can chain them into a single readable block.
Real World Context
When an endpoint changes, one clear assertion failure — Expected 201, got 422 — tells you exactly what broke. Vague assertTrue(true) noise tells you nothing. The right assertion is half the battle.
Deep Dive
Response Assertions
Chain assertions to check status, content, and session state in one readable block:
phpuse function Pest\Laravel\{get, post}; it('returns JSON for the projects API', function () { $response = get('/api/projects'); $response ->assertStatus(200) ->assertHeader('Content-Type', 'application/json') ->assertJsonCount(3, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'name', 'status'], ], ]); }); it('redirects after storing a project', function () { post('/projects', ['name' => 'New']) ->assertRedirect('/projects') ->assertSessionHas('success', 'Project created!'); });
Common response assertions you'll reach for constantly:
| Assertion | Checks |
|---|---|
assertStatus(200) | HTTP status code |
assertOk() | Shortcut for status 200 |
assertRedirect('/path') | Response redirects to /path |
assertSee('text') | Response body contains the text |
assertDontSee('error') | Response body does NOT contain the text |
assertJson([...]) | Response JSON contains the given fragment |
assertJsonStructure([...]) | Response JSON matches the given shape |
assertSessionHasErrors([...]) | Session contains validation errors |
Database Assertions
phpuse function Pest\Laravel\{assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount}; it('persists the project', function () { post('/projects', ['name' => 'Launch Website']); assertDatabaseHas('projects', ['name' => 'Launch Website']); assertDatabaseCount('projects', 1); });
assertDatabaseHas queries the database directly — perfect for verifying side effects that the response body does not reveal.
Common Pitfalls
- Asserting on implementation details —
assertDatabaseHasis fine, butassertSeeon CSS class names is brittle. Assert on user-visible text or API shape, not DOM internals. - Skipping
RefreshDatabase— Flaky tests usually come from leftover data. Always isolate state.
Best Practices
- Chain assertions —
assertStatus(200)->assertSee('Welcome')reads top-to-bottom like a spec. - Use
assertJsonStructurefor APIs — It locks the contract's shape without tying you to exact values.
Summary
- Response assertions cover status, headers, body, session, and JSON shape.
- Database assertions verify rows exist, are missing, or match a count.
- Chain assertions into a single readable block.
- Assert on behavior and shape, not implementation details.
Code Examples
php
<?php
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\{get, assertDatabaseHas};
uses(RefreshDatabase::class);
it('lists existing projects', function () {
Project::factory()->create(['name' => 'Launch Website']);
Project::factory()->create(['name' => 'Hire Designer']);
get('/projects')
->assertOk()
->assertSee('Launch Website')
->assertSee('Hire Designer');
assertDatabaseHas('projects', ['name' => 'Launch Website']);
});