Feature Tests

Send a real request through the kernel to a route, then assert the status, redirect, session or JSON that came back.

Syntax$this->postJson(string $uri, array $data = [], array $headers = []): Illuminate\Testing\TestResponse

A feature test boots the application and pushes a request through the full HTTP stack: middleware, route matching, controller, validation, response. It is the test that catches real breakage, because it exercises the wiring rather than one method in isolation.

Making the request

$this->get(), post(), put(), patch() and delete() take a URI and return an Illuminate\Testing\TestResponse. The JSON variants add the JSON Accept and Content-Type headers for you: $this->postJson('/api/posts', $data) is the same call as $this->json('POST', '/api/posts', $data). withHeaders() and withSession() prepare the request. from('/posts/create') sets the referring URL, which matters because a redirect back goes there.

Asserting on the response

assertStatus(200) always works, and the named forms are the same assertion: assertOk, assertCreated, assertNoContent, assertNotFound, assertForbidden, assertUnprocessable. For redirects, assertRedirect('/posts') or assertRedirectToRoute('posts.index'). For the session, assertSessionHas($key, $value = null). For JSON, assertJsonPath('data.0.title', 'Hello') and assertJsonCount($count, $key = null). For Blade output, assertSee(), assertViewIs() and assertViewHas().

Know what is converting your error

Exceptions thrown inside the application are still turned into HTTP responses by the exception handler, so a bug arrives as a 500 and the real message is hidden. withoutExceptionHandling() before the request lets the exception reach the test. A validation failure is a 302 back with errors flashed to the session for a browser request, and a 422 with a JSON error body for a JSON request. assertInvalid(['title']) reads both shapes; assertSessionHasErrors() only reads the session one.

Example

Example · php
<?php
// tests/Feature/PostFormTest.php

declare(strict_types=1);

namespace Tests\Feature;

use Tests\TestCase;

class PostFormTest extends TestCase
{
    public function test_the_create_form_renders(): void
    {
        $response = $this->get('/posts/create');

        $response->assertOk();
        $response->assertSee('Publish');
        $response->assertViewIs('posts.create');
    }

    public function test_a_post_without_a_title_is_rejected(): void
    {
        $response = $this->from('/posts/create')->post('/posts', [
            'title' => '',
            'body' => 'Draft body',
        ]);

        // The redirect goes back to the URL set by from().
        $response->assertRedirect('/posts/create');

        // assertInvalid reads session errors and JSON errors alike.
        $response->assertInvalid(['title']);
        $response->assertValid(['body']);
    }
}

When to use it

  • Prove a form rejects bad input and sends the user back with the right error keys.
  • Lock an API's response shape in place before refactoring the controller behind it.
  • Reproduce a reported 500 as a failing test, then fix it.
  • Check that a route is still reachable after a middleware change.

More examples

A JSON endpoint

The same controller returns a different failure shape for JSON clients: 422 with an errors object instead of a 302 and a session flash. Reach for the Json methods whenever the route is consumed by an API client, because they set the Accept header that decides which shape you get.

Example · php
<?php
// tests/Feature/Api/PostApiTest.php

declare(strict_types=1);

namespace Tests\Feature\Api;

use Tests\TestCase;

class PostApiTest extends TestCase
{
    public function test_creating_a_post_returns_201_and_the_resource(): void
    {
        $response = $this->postJson('/api/posts', [
            'title' => 'Hello',
            'body' => 'First post',
        ]);

        $response->assertCreated();                       // assertStatus(201)
        $response->assertJsonPath('data.title', 'Hello');
    }

    public function test_an_invalid_payload_returns_422(): void
    {
        $response = $this->postJson('/api/posts', ['title' => '']);

        $response->assertUnprocessable();                  // assertStatus(422)
        $response->assertJsonValidationErrors(['title']);
    }
}

Seeing the real exception

Use this while diagnosing a red test, then take it out. Leaving it in permanently means tests stop covering how your application presents errors to users.

Example · php
public function test_the_report_page_loads(): void
{
    // Without this line a thrown exception becomes a 500 response
    // and the assertion failure tells you nothing useful.
    $response = $this->withoutExceptionHandling()->get('/reports/monthly');

    $response->assertOk();
}

Session state after a redirect

withSession() seeds the session before the request, which is how you test a route that depends on something the previous page put there. assertSessionHasNoErrors() is worth adding to happy-path tests: it catches a silent validation failure that would otherwise look like a successful redirect.

Example · php
public function test_publishing_flashes_a_status_message(): void
{
    $response = $this->withSession(['team_id' => 7])
        ->post('/posts/1/publish');

    $response->assertRedirectToRoute('posts.show', ['post' => 1]);
    $response->assertSessionHas('status', 'Post published.');
    $response->assertSessionHasNoErrors();
}

Discussion

  • Be the first to comment on this lesson.