Testing Laravel Form Requests

Laravel provides a fluent API for HTTP tests. This API allows us to send a request and perform assertions on the response. In this post, we explore how to test Laravel form request validation.

We start with a test for the controller of a sample JSON API. Therefore, we scaffold a new integration test using the Artisan make:test CLI command. The test will be in the tests directory of the Laravel application and mirror the architecture of the main application.

php artisan make:test Http/Controllers/Api/UsersControllerTest

We are going to use a (slightly modified) example from the section Testing JSON APIs from the Laravel documentation:

UsersControllerTest.php
use WithFaker;
/** @test */
public function a_user_can_create_a_new_user()
{
    $user = factory(User::class)->make();
    $response = $this
        ->actingAs($user)
        ->postJson(route('api.user.store'), [
            'name' => $this->faker->name,
            'email' => $this->faker->safeEmail,
        ]);
    $response
        ->assertStatus(201)
        ->assertJson([
            'created' => true,
        ]);
}

In this test, we act as an authenticated user to send an HTTP POST request to the JSON API endpoint. In the response, we assert the returned HTTP status as well as the returned JSON data.

This test covers the happy path. This means we send valid data (including a user, a name, and an email address) and therefore receive a valid response. This test does not cover the validation of the attributes, like an empty name, or a malformed email address.

Form Request Validation

Validation has its own place in Laravel know as form request validation.

Again, we stick to the example from the documentation and create a form request class, using the Artisan make:request CLI command:

php artisan make:request StoreUser

The authorize() method checks that only authenticated users are authorized to make the request.

The rules() method of the form request object is the place where we can set up all the validation rules for the API controller:

StoreUser.php
public function authorize(): bool
{
    return Auth::check();
}
public function rules(): array
{
    return [
        'name' => ['required', 'max:255'],
        'email' => ['required', 'unique:users,email', 'email:rfc,dns'],
    ];
}

Now it's time to take a closer look at the controller. We utilize the Artisan make:controller CLI command to scaffold the API controller:

php artisan make:controller Api/UsersController

We can now type-hint the form request in the API controller action and apply the rules defined in the form request object:

UsersController.php
public function store(StoreUser $request): JsonResponse
{
    // Handle the valid request.
    $validated = $request->validated();
    // …
    return response()
        ->json([
            'created' => true,
        ], 201);
}

The incoming $request will be validated against the rules provided in the StoreUser class.

If the validation fails, the user will be redirected back. If the request is an AJAX request, an HTTP response with the status code 422 will be returned including all the validation errors.

With the validation being separated from the controller (in its own form request class), how can we make sure the controller method uses the appropriate form request validation?

Testing Form Requests

It turns out Jason McCreary, the creator of Laravel Shift, has built a neat package to solve this issue. The Laravel Test Assertions package provides a set of helpful assertions when testing Laravel applications.

Let's install the package (currently v1.0.0) and give it a try:

composer require --dev jasonmccreary/laravel-test-assertions

We add the trait AdditionalAssertions at the beginning of the test and can now assert that the action for a given controller is linked to the appropriate form request object:

UsersControllerTest.php
use AdditionalAssertions;
/** @test */
public function store_uses_form_request()
{
    $this->assertActionUsesFormRequest(
        UsersController::class,
        'store',
        StoreUser::class
    );
}

This test gives us confidence that the controller uses the associated form request. We can now be sure the controller action performs the expected validations.

Please note that we do not test the validation rules here! The test only makes sure that the controller uses the form request.

Testing Form Request Validation Rules

While we do not need to test the rules provided by Laravel, we still want to make sure the rules are in place. Let's write another test for the form request:

php artisan make:test Http/Requests/StoreUserTest

The Laravel Test Assertions package offers an assertion to quickly test if the validation rules match:

StoreUserTest.php
use AdditionalAssertions;
/** @test */
public function it_verifies_validation_rules()
{
    $formRequest = new StoreUser();
    $this->assertExactValidationRules([
        'name' => ['required', 'max:255'],
        'email' => ['required', 'unique:users,email', 'email:rfc,dns'],
    ], $formRequest->rules());
}

Of course, this test might seem a little brittle on the first look. The test is going to fail as soon as the rules, or the fields are changing. However, changing the rules or the fields should always result in a failing test!

Further reading: Jason McCreary wrote about the real tradeoffs in his post Testing validation in Laravel by asserting a form request.

Testing Form Request Authorization

The authorize() method in the form request determines if the incoming request is an authorized request. We can (and should!) test this behavior as well:

StoreUserTest.php
/** @test */
public function it_verifies_authorized()
{
    $user = factory(User::class)->make();
    $this->actingAs($user);
    $formRequest = new StoreUser();
    $this->assertTrue($formRequest->authorize());
}

In the test, we act as an authenticated user and new up the form request. We perform the assertion against the returned value of the authorize() method. This test gives us confidence that we only allow authorized requests.

Writing Tests Takes Time

Laravel is built with testing in mind. Yet testing a controller method with a lot of fields can get out of hand quite fast. The Laravel Test Assertions package offers an option to quickly knock off the main points and gives us confidence that everything is wired up correctly. With the basics covered we can focus on more complex parts of the application like custom validation rules or the business logic of the application.

Keep testing until you have confidence. Jason McCreary

Credits for this post are going to Jason McCreary for his awesome Confident Laravel course, a step-by-step guide to writing tests for Laravel applications.