PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
167: Testing HTTP Endpoints in Laravel
I’ve been working on this Order Status endpoint for a client. The logic is simple: you send an order ID, and it tells you if the package is 'processing', 'shipped', or 'delivered'. Now, I could just keep hitting the endpoint with Postman, but as soon as I touch the database logic, I'm going to break something. Let's automate this.
The "It works on my machine" trap
I'll start by writing a basic test. I have my route set up at /api/orders/{id}/status. My first instinct is to just hit that URL and see if I get a 200 OK response. Here is my first attempt at a test method:
public function test_order_status_endpoint_returns_success()
{
$response = $this->getJson('/api/orders/1/status');
$response->assertStatus(200);
}
I run php artisan test and... it fails. 404 Not Found. Now, if I check my browser or Postman, it works fine. Why? Because my local development database has an order with ID 1, but the testing environment uses a separate, clean SQLite database in memory. It's empty. I'm testing a ghost.
Wait, where did the data go?
I can't hardcode IDs in tests; that's a recipe for disaster. I need to actually create the data I'm testing against within the test itself. Since we've already set up Factories in earlier lessons, I'll use one to spin up a real order on the fly.
public function test_order_status_endpoint_returns_success()
{
// Create a real order in the test database
$order = Order::factory()->create(['status' => 'shipped']);
// Use the ID from the created model
$response = $this->getJson("/api/orders/{$order->id}/status");
$response->assertStatus(200);
}
That passes. But here is the thing: a 200 status code only tells me the server didn't crash. It doesn't tell me if the API actually returned the correct status. It could be returning a blank page and still be a "success" in the eyes of the HTTP protocol. I need to peek inside the JSON payload.
Checking the payload
I want to make sure the JSON contains the key status and that its value matches what I put in the factory. Laravel gives us assertJson for this, which is a lifesaver. I'll tweak the test to be more specific.
public function test_order_status_returns_correct_data()
{
$order = Order::factory()->create(['status' => 'processing']);
$response = $this->getJson("/api/orders/{$order->id}/status");
$response->assertStatus(200)
->assertJson([
'status' => 'processing',
]);
}
Now we're talking. If I accidentally change the controller to return 'pending' instead of 'processing', this test will catch it. It's no longer just checking if the door is open; it's checking if the right person is standing behind the door.
Handling the missing pieces
Finally, I have to think about the "sad path." What happens if a user requests an order ID that doesn't exist? If I haven't handled this in my controller, Laravel might throw a generic 500 error or a messy HTML exception page. For an API, that's unacceptable—we need a clean 404 JSON response.
I'll write a test for the failure state first. This is basically Test-Driven Development (TDD) at this point.
public function test_order_status_returns_404_for_invalid_order()
{
// We don't create any order here
$response = $this->getJson('/api/orders/99999/status');
$response->assertStatus(404);
}
This fails with a 500 error because my controller is currently just calling Order::findOrFail($id) without a try-catch block or a proper handler for the API. I'll go back to the controller, wrap that logic, or let the Laravel exception handler take over by ensuring the request header is Accept: application/json (which getJson does automatically). Once I've fixed the controller to return a proper 404, this test turns green.
By testing both the "Happy Path" and the "Sad Path," I can now refactor my controller logic with total confidence. If I break the 404 handling while trying to optimize the 200 response, the tests will scream at me immediately.
📋 Practical Task
Exercise: Validating the Product Inventory API
You have been handed a Laravel project with a ProductController that has an endpoint: GET /api/products/{id}/inventory. This endpoint should return the current stock level of a product.
Your Goal: Write a test class ProductInventoryTest that implements the following three scenarios:
- The Success Case: Create a Product using a factory with a specific
stock_count(e.g., 15). Assert that the endpoint returns a 200 status and a JSON response containing{"stock": 15}. - The Empty Case: Create a Product with a
stock_countof 0. Assert that the endpoint still returns a 200 status, but the JSON reflects the 0 stock. - The Missing Product Case: Request an ID that does not exist in the database. Assert that the endpoint returns a 404 status.
Tip: Ensure you are using $this->getJson() to simulate the API request and that you are creating your models inside the test methods to avoid database dependency issues.
There are no comments for now.