Skip to main content
Create your own
Lesson illustration

Testing API Endpoints with Feature Tests

Hello! Welcome back to our module on Production-Ready Testing.

In the last lesson, we established the fundamental difference between unit and feature tests. We learned that feature tests are a form of integration test that examines a piece of functionality from the outside in, just like a user or an API client. They are crucial for ensuring that all the different parts of your application—controllers, models, database, etc.—work together correctly.

Today, we will put that theory into practice. This lesson is dedicated to one of the most common and critical tasks for a backend developer: testing API endpoints. Our goal is to write feature tests for API endpoints, asserting status codes and JSON structure. By the end of this lesson, you'll be able to create tests that give you high confidence in the reliability and correctness of your application's API.


1. The Anatomy of an API Feature Test

Before we write code, let's understand the process. When testing an API endpoint, we follow the same "Arrange, Act, Assert" pattern we discussed previously, but with a specific focus:

  1. Arrange: Prepare the application state. This usually involves using model factories to create specific data in our test database.
  2. Act: Make an HTTP request to the API endpoint. Laravel provides a suite of helper methods like getJson(), postJson(), etc., to simulate these requests without actually hitting a web server.
  3. Assert: Examine the response. We will check two main things:
    • The HTTP Status Code: Was the request successful (200 OK, 201 Created)? Did it fail as expected (404 Not Found, 422 Unprocessable Entity)?
    • The JSON Response: Does the response body contain the correct data? Does it have the right structure (the expected keys and nesting)?

Let's look at the kind of response we aim to verify.

Laravel API JSON Response Example
This is a typical JSON response from a Laravel API. Our feature tests will programmatically verify that the status is '200 OK' and that the JSON body contains the expected data and structure.

Creating the Test File

To begin, you generate a new feature test file using an Artisan command. By convention, test file names often mirror the controller they are testing. For example, to test ProductController, you would run:

php artisan make:test ProductApiTest

This command creates a new file in tests/Feature/ProductApiTest.php. Inside your tests, you'll typically use the RefreshDatabase trait, which ensures your database is reset to a clean state before each test runs. This is essential for preventing tests from interfering with one another.

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProductApiTest extends TestCase
{
    use RefreshDatabase;

    // Our tests will go here...
}

2. Testing Core API Endpoints

Let's see this in action by testing the two most fundamental API actions: retrieving a list of resources (GET) and creating a new resource (POST).

Laravel Testing 16/24: Testing APIs and JSONs

The following video from the Laravel Daily channel provides a clear, practical walkthrough of testing a simple API for products. It covers getting a list, creating a product successfully, and handling a failed creation due to validation errors.

Watch from 00:56 to 05:36. Pay close attention to the methods being used: getJson() and postJson() to make the requests. Model factories (Product::factory()->create()) to arrange the data. Assertions like assertStatus() and assertJson() to verify the response.

Let's break down the key takeaways from that video.

Testing a GET Endpoint (List/Index)

When testing an endpoint that returns a list of items, you typically want to check:

  1. The request is successful (Status 200).
  2. The JSON response contains the data you created in the "Arrange" step.

Here is a code example that captures the essence of the video's first test:

use App\Models\Product;

test('api returns a list of products', function () {
    // Arrange: Create a product in the database.
    $product = Product::factory()->create();

    // Act: Make a GET request to the API endpoint.
    $response = $this->getJson('/api/products');

    // Assert
    $response
        ->assertStatus(200) // Or ->assertOk()
        ->assertJson([       // Assert the response contains this JSON fragment.
            'data' => [
                [
                    'name' => $product->name,
                    'price' => $product->price,
                ]
            ]
        ]);
});

Note: The exact JSON structure ('data' => [...]) depends on how you've set up your API Resources, which we'll cover in a later module. The principle remains the same.

Testing a POST Endpoint (Create/Store)

Testing a creation endpoint involves a "happy path" (successful creation) and an "unhappy path" (validation failure).

  • Happy Path:

    • Send valid data using postJson().
    • Assert for a 201 Created status code.
    • Assert that the response contains the data of the newly created resource.
    • (Optional but recommended) Assert that the data now exists in the database using assertDatabaseHas().
    test('api can create a new product', function () {
        $productData = [
            'name' => 'New Awesome Gadget',
            'price' => 199.99
        ];
    
        $this->postJson('/api/products', $productData)
            ->assertStatus(201) // Or ->assertCreated()
            ->assertJsonFragment($productData); // Checks if this fragment exists in the response
    });
    
  • Unhappy Path (Validation):

    • Send invalid data (e.g., a missing required field).
    • Assert for a 422 Unprocessable Entity status code, which Laravel uses for validation errors.
    • Assert that the response includes a specific validation error message.
    test('api returns validation error if name is missing', function () {
        $productData = ['price' => 99.99]; // Missing 'name'
    
        $this->postJson('/api/products', $productData)
            ->assertStatus(422) // Or ->assertUnprocessable()
            ->assertJsonValidationErrorFor('name');
    });
    

3. Deep Dive into JSON Assertions

Laravel's testing tools provide a rich set of assertions specifically for JSON responses. While assertJson is a good start, there are more precise tools available.

HTTP Tests - Testing JSON APIs

The official Laravel documentation is the best place to explore the full range of possibilities for testing JSON APIs. Please read the following sections to understand the different assertion methods available to you.

Read the sections titled 'Testing JSON APIs', 'Asserting Exact JSON Matches', 'Asserting on JSON Paths', and 'Fluent JSON Testing'. Focus on the differences between assertJson, assertExactJson, and the powerful fluent interface provided by AssertableJson.

Here's a summary of the key methods you just read about:

Method Purpose
assertJson($array) Asserts that the response contains the given JSON fragment. The response can have extra keys.
assertExactJson($arr) Asserts that the response JSON exactly matches the given array. No more, no less.
assertJsonPath($path, $value) Asserts that a specific key (using dot notation) has a specific value. Example: assertJsonPath('data.0.name', 'My Product').
assertJsonCount($count, $key) Asserts that a JSON array at a given key has a specific number of items. Example: assertJsonCount(5, 'data').
assertJson(fn...) The fluent JSON assertion, which gives you an AssertableJson object for powerful, chainable assertions on structure, types, and values.

4. Asserting JSON Structure

Sometimes, you care more about the shape of your API response than its specific content. You want to guarantee that your API contract is being upheld—that is, it always returns the expected fields. This is the job of assertJsonStructure().

Laravel Feature Test for API Endpoint
This example shows a perfect use case for `assertJsonStructure`. The test verifies that each item in the `data` array has an `id`, `name`, and `email`, without checking their actual values.

This is especially powerful when testing collections, where you can use a wildcard (*) to specify the structure for every item in an array.

Asserting a JSON Response Structure in Laravel

This article from Laravel News provides an excellent, focused look at how and why to use assertJsonStructure and how it complements other assertions.

Read the full article. It's short and will clarify how to use assertJsonStructure for collections and how to use whereType assertions to make your tests even more robust.

As the article explained, you can combine these techniques. A comprehensive test for a list endpoint might look like this:

use Illuminate\Testing\Fluent\AssertableJson;

test('api for products has the correct structure and types', function () {
    // Arrange: Create 3 products
    Product::factory()->count(3)->create();

    // Act & Assert
    $this->getJson('/api/products')
        ->assertOk()
        ->assertJsonCount(3, 'data')
        ->assertJsonStructure([
            'data' => [
                '*' => [ // The '*' means "every item in this array"
                    'id',
                    'name',
                    'price',
                    'created_at',
                    'updated_at',
                ]
            ]
        ])
        ->assertJson(fn (AssertableJson $json) =>
            $json->whereType('data.0.id', 'integer')
                 ->whereType('data.0.name', 'string')
                 ->etc() // etc() allows other keys to be present
        );
});

This test confirms:

  1. The request was successful.
  2. There are exactly 3 products in the data array.
  3. Every product object in the data array has the expected keys.
  4. The data types for the first product's id and name are correct.

This layered approach gives you extremely high confidence in your API's output.


Conclusion

In this lesson, you've moved from theory to practice, learning how to write robust feature tests for your Laravel API endpoints. This is a non-negotiable skill for building professional, maintainable applications.

Key Takeaways:

  • API Testing Workflow: Follow the Arrange, Act, Assert pattern by creating data with factories, making requests with getJson/postJson, and asserting the response.
  • Status Code Assertions: Use methods like assertOk(), assertCreated(), assertNotFound(), and assertUnprocessable() to verify the HTTP status.
  • JSON Content Assertions: Use assertJson() for partial matches, assertExactJson() for strict matches, and assertJsonFragment() to find data anywhere in the response.
  • JSON Structure Assertions: Use assertJsonStructure() with wildcards (*) to enforce your API contract and ensure consistency in your responses.
  • Type Assertions: Add another layer of validation by using whereType() within a fluent JSON assertion to check data types.

Up Next:

We've now mastered testing our application from the "outside-in" with feature tests. In the next lesson, we will zoom in and focus on the "inside." You will learn how to write unit tests for individual classes and methods, isolating their logic. This will equip you with the skills to test complex business logic and helper functions quickly and efficiently, completing the foundation of your testing knowledge.

Can't find a good explanation? Sign up and we'll make it for you

Sign up