Feature Tests
Send a real request through the kernel to a route, then assert the status, redirect, session or JSON that came back.
$this->postJson(string $uri, array $data = [], array $headers = []): Illuminate\Testing\TestResponseA 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
<?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.
<?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.
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.
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