Introduction

I recently came across some issues with testing JSON API endpoints in my Laravel application using the desginated testing methods.

A simple test case would look like the following.

/** @test */
public function it_tests_that_a_user_can_successfully_create_a_new_post()
{
    // Easy, works
    $this->actingAs($user)
        ->post('/api/posts', [
            'title' => 'This is the post title',
            'body'  => 'This is the post body'
        ])
        ->see('Post was successfully created')
        ->assertResponse(201);
}

As this test is expected to pass, you won't think much more of it.

But as you're a good developer, you're also performing some kind of validation on your POST /api/posts endpoint, to ensure that any require fields are going to be present and meet your application's criteria.

The problem

/** @test */
public function it_tests_that_required_fields_are_property_validated()
{
    $this->actingAs($user)
        ->post('/api/posts', [
            'title' => 'This is my post title',
            'body'  => null
        ])
        ->see('The body field is reqiured')
        ->assertResponseStatus(422);
}

You would expect to get a 422 (Unprocessable Entity) HTTP status code returned, and that the given validation errors are present in the returned JSON.

This is where you might start to run into issues.

Instead of getting the expected 422 error, you're going to receive a 302 (Found) HTTP status code and be redirected to a HTML page. This is just fine in a browser session, as you're likely to be displaying flashed messages to the user, but within your API, this is undesired.

The solution(s)

It's important to understand why this seemingly unexpected behaviour occurs, before breaking down two ways to correctly test your JSON API.

This unexpected behaviour occurs because Laravel doesn't know you're making an AJAX request when you use the post() method and redirects you back (302) with the errors flashed to the session as though you were visiting in your browser.

In order to correctly test this behaviour, you need to explicitly tell Laravel that you expect a JSON response back. You can make this declaration by passing the Accept header in the post method as the third parameter - an array of HTTP key/value pairs.

/** @test */
public function it_tests_that_required_fields_are_property_validated()
{
    $this->actingAs($user)
        ->post('/api/posts', [
            'title' => 'This is my post title',
            'body'  => null,
        ], [
            'Accept' => 'application/json',
        ])
        ->see('The body field is reqiured')
        ->assertResponseStatus(422);
}

Now that you understand this behaviour, you can continue to pass the Accept header directly, or use the simple json() request method.

/** @test */
public function it_tests_that_required_fields_are_property_validated()
{
    $this->actingAs($user)
        ->json('POST', '/api/posts', [
            'title' => 'This is my post title',
            'body' => null,
        ])
        ->see('The body field is required')
        ->assertResponseStatus(422);
}

The json() method will automatically handle making a request with the given method (POST) to your API with the appropriate JSON-encoded data, as well as setting the relevant HTTP headers.

Digging further

Laravel's ValidatesRequests trait is responsible for handling the validation within your application, if you're using the controller's validate() method.

If the validation fails, it will call throwValidationException, which in turn returns a HttpResponseException. When this exception is built using the buildFailedValidationResponse, a check is made to determine if the incoming request is ajax() or if it wantsJson() in response.

It is because of this check that we need to either explicitly tell Laravel that we want JSON with the Accept header, or by using the json() method in your test. Without either, the check fails, and you are simply redirected to the previous URL with your existing input and any errors.

The alternative to using the Accept header is by using ['HTTP_X-Requested-With' => 'XMLHttpRequest'] instead, which is what is used by Laravel to determine if an AJAX request was made.

If you are using something like vue-resource or jQuery.ajax(), the HTTP_X-Requested-With header will automatically be appended.