Back to Blog
Lesson 16 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20264 min read

Feature Testing Fundamentals: Automating Laravel API Verification

Master feature testing in Laravel. Learn to set up your environment, write tests for API endpoints, and assert JSON responses to build rock-solid applications.

LaravelPHPUnitTestingAPIPestphpbackend

Previously in this course, we explored asynchronous processing with queues in Laravel to offload heavy tasks. While background jobs improve performance, they also introduce complexity that requires automated verification. This lesson shifts our focus to feature testing, ensuring that our API endpoints behave exactly as expected before we ship code to production.

If you haven't already, you might want to review our introduction to testing to understand the philosophy behind testing in Laravel.

Understanding Feature Tests

While unit tests focus on isolated methods or classes, feature tests cover the entire request-response lifecycle. When we test an API endpoint, we aren't just testing the controller method; we are testing the route, the middleware, the form request validation, the service layer, and the final JSON response.

In a professional Laravel environment, feature tests are your primary safety net. They allow you to refactor code—like moving logic into a service class—with the confidence that you haven't broken the user-facing API.

Setting Up PHPUnit and Pest

Laravel ships with Pest as the default testing framework, which sits on top of PHPUnit. To get started, ensure your phpunit.xml file exists in the root of your project. If you are using a modern Laravel installation, this is already configured.

To create your first feature test, run:

Bash
php artisan make:test TaskApiTest --pest

This generates a file in tests/Feature/TaskApiTest.php. Using the --pest flag gives us a cleaner, more readable syntax compared to traditional PHPUnit classes.

Writing Your First API Feature Test

Let's test our project board's ability to fetch a list of tasks. A good feature test follows the "Arrange, Act, Assert" pattern.

PHP
#6A9955">// tests/Feature/TaskApiTest.php

use App\Models\User;
use App\Models\Task;

test('authenticated user can view their tasks', function () {
    #6A9955">// 1. Arrange: Create a user and some tasks
    $user = User::factory()->create();
    Task::factory()->count(3)->for($user)->create();

    #6A9955">// 2. Act: Send a GET request to the index endpoint
    $response = $this->actingAs($user, 'sanctum')
                     ->getJson('/api/v1/tasks');

    #6A9955">// 3. Assert: Verify the response
    $response->assertStatus(200)
             ->assertJsonCount(3, 'data')
             ->assertJsonStructure([
                 'data' => [
                     '*' => ['id', 'title', 'status']
                 ]
             ]);
});

Breaking Down the Assertions

  • actingAs($user, 'sanctum'): This helper authenticates the request as if a user provided a valid Sanctum token.
  • getJson(...): This sends an HTTP GET request and expects a JSON response, setting the Accept: application/json header automatically.
  • assertJsonCount(3, 'data'): Since our API uses resources, it typically returns data nested under a data key. This ensures the count is correct.
  • assertJsonStructure(...): This validates the shape of your JSON output without forcing you to hardcode every specific value, which is useful for dynamic data like timestamps.

Hands-on Exercise

Now it's your turn. In our project board, we have a POST /api/v1/tasks endpoint. Create a test file named CreateTaskTest.php and write a test that:

  1. Creates an authenticated user.
  2. Sends a POST request with valid task data (e.g., ['title' => 'New Task']).
  3. Asserts that the response status is 201 Created.
  4. Asserts that the database now contains the record using assertDatabaseHas('tasks', ['title' => 'New Task']).

Common Pitfalls to Avoid

Testing is powerful, but it’s easy to fall into traps that make your suite fragile:

  • Testing Implementation Details: Don't test that a specific service method was called if you can avoid it. Test the result of the request. If you change your internal logic but the API output remains the same, your test should still pass.
  • Over-asserting JSON: Avoid assertJson(['id' => 1, ...]) for large objects. It makes tests brittle because they break whenever a unrelated field changes. Use assertJsonStructure or assertJsonPath instead.
  • Database Pollution: Ensure your tests use the Illuminate\Foundation\Testing\RefreshDatabase trait. This ensures your database is migrated and cleared between every single test, preventing "leaky" state where one test affects the outcome of another.

Recap

Feature tests are the cornerstone of a maintainable API. By simulating real HTTP requests, we ensure our middleware, validation, and business logic work in concert. We've covered:

  1. Setting up the test environment with Pest.
  2. Using actingAs to simulate authentication.
  3. Validating JSON structure and data counts.
  4. The importance of RefreshDatabase for clean test states.

Automated testing is not an optional "extra"—it is the only way to scale a production application without constant manual regressions.

Up next: We will dive into Mocking Services and Repositories in Tests, where we'll learn how to isolate our controllers from complex external dependencies to make our test suite run lightning fast.

Similar Posts