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.
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:
Bashphp 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 theAccept: application/jsonheader automatically.assertJsonCount(3, 'data'): Since our API uses resources, it typically returns data nested under adatakey. 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:
- Creates an authenticated user.
- Sends a POST request with valid task data (e.g.,
['title' => 'New Task']). - Asserts that the response status is
201 Created. - 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. UseassertJsonStructureorassertJsonPathinstead. - Database Pollution: Ensure your tests use the
Illuminate\Foundation\Testing\RefreshDatabasetrait. 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:
- Setting up the test environment with Pest.
- Using
actingAsto simulate authentication. - Validating JSON structure and data counts.
- The importance of
RefreshDatabasefor 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.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.