
Laravel REST API Part 6: Unit Testing Products List API
Published on Dec 9, 2024
In the previous episode, we developed our Products List API. This episode will focus on improving its reliability by writing unit test cases. By the end, you'll understand how to create a test class, implement test cases, and validate API behavior effectively.
1. Creating the Test Class
To start, we need to generate a test class. Using Laravel's Artisan command, we create a new test class for the ProductController:
php artisan make:test ProductControllerTest
This command generates a new test file in the tests/Feature directory, where we’ll write our test cases.
2. Running Initial Tests
Before writing new test cases, it’s essential to ensure that existing ones are working as expected. Run the tests using:
php artisan test
This ensures the test environment is stable before proceeding with further additions.
3. Setting Up the Test Environment
We use Laravel's RefreshDatabase trait to ensure each test runs on a fresh database instance. Then, we define a setup method to create test data:
protected function setUp(): void { parent::setUp(); $this->user = User::factory()->create(); $this->category1 = Category::factory()->create(['name' => 'Electronics']); $this->category2 = Category::factory()->create(['name' => 'Books']); Product::factory()->create([ 'name' => 'iPhone', 'price' => 1000, 'stock' => 50, 'category_id' => $this->category1->id ]); Product::factory()->create([ 'name' => 'Samsung Galaxy', 'price' => 900, 'stock' => 30, 'category_id' => $this->category1->id ]); Product::factory()->create([ 'name' => 'Laravel For Dummies', 'price' => 20, 'stock' => 100, 'category_id' => $this->category2->id ]); }
This setup ensures that the database contains a user, two categories, and three products for testing.
4. Testing API Response
Smoke Test: Checking API Availability
The first test ensures the API endpoint responds correctly:
public function test_can_list_products(): void { $response = $this->actingAs($this->user)->get('/api/products'); $response->assertStatus(200) ->assertExactJsonStructure([ 'data' => [ '*' => [ 'id', 'name', 'price', 'stock', 'created_at', 'category' => [ 'id', 'name', ], ] ], 'links' => ['first', 'last', 'prev', 'next'], 'meta' => [ 'current_page', 'from', 'last_page', 'links' => [ '*' => [ 'url', 'label', 'active' ] ], 'path', 'per_page', 'to', 'total' ], ]); }
This test:
- Authenticates the user with actingAs.
- Sends a GET request to the products endpoint.
- Verifies the response status (200 OK).
- Checks the JSON structure for consistency.
Handling Authentication
If the actingAs method is removed, the API returns a 401 Unauthorized error. This ensures that only authenticated users can access the endpoint.
5. Testing Filters
The API supports various filters. Below are the test cases for each filter:
Filter by Product Name
public function test_can_filter_by_product_name() { $response = $this->actingAs($this->user)->json('GET', '/api/products', [ 'product_name' => 'iPhone' ]); $response->assertStatus(200) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'name' => 'iPhone', 'price' => 1000, 'stock' => 50, ]); }
Filter by Price Range
public function test_can_filter_by_price_range() { $response = $this->actingAs($this->user)->json('GET', '/api/products', [ 'price_from' => 100, 'price_to' => 1000 ]); $response->assertStatus(200) ->assertJsonCount(2, 'data') ->assertJsonFragment(['name' => 'iPhone', 'stock' => 50, 'price' => 1000]) ->assertJsonFragment(['name' => 'Samsung Galaxy', 'stock' => 30, 'price' => 900]); }
Filter by Stock Range
public function test_can_filter_by_stock_range() { $response = $this->actingAs($this->user)->json('GET', '/api/products', [ 'stock_from' => 25, 'stock_to' => 45 ]); $response->assertStatus(200) ->assertJsonCount(1, 'data') ->assertJsonFragment(['name' => 'Samsung Galaxy', 'stock' => 30, 'price' => 900]); }
Filter by Category
public function test_can_filter_by_category_id() { $response = $this->actingAs($this->user)->json('GET', '/api/products', [ 'category_id' => $this->category2->id ]); $response->assertStatus(200) ->assertJsonCount(1, 'data') ->assertJsonFragment(['name' => 'Laravel For Dummies', 'price' => 20, 'stock' => 100]); }
Why Unit Tests Matter
Unit tests ensure stability during development. While real-world projects might not mandate exhaustive test coverage due to time constraints, simple cases like verifying API responses and structures are crucial. They:
- Detect breaking changes early.
- Allow safe refactoring.
- Prevent unexpected production issues.
Even a basic test case that checks for a 200 OK response and expected JSON structure can save hours of debugging.
Testing ensures your APIs remain robust and reliable, even as features evolve. Stay tuned for the next episode, where we’ll dive deeper into improving API functionality!