Two weeks ago, I was finalizing a Laravel API for a logistics company in Dubai. Their app tracked delivery routes and integrated with Google Sheets for real-time updates. Everything looked good in Postman. But when we pushed to staging, half the endpoints returned 500 errors. Turns out, the database driver in the Docker container didn’t match local. I ended up rewriting several PHPUnit tests that afternoon to catch this earlier next time.
Let’s be real: API testing isn’t glamorous. But after 7 years (and more than my share of 2AM dd() debugging sessions), I’ve settled into a workflow that saves hours of headaches. This isn’t some idealized tutorial setup — I’m telling you how it works on actual projects for UAE clients.
Setting Up the Testing Environment
I always start with Laravel’s native PHPUnit integration. It’s baked in since Laravel 10, so there’s zero setup time. My typical stack:
- •PHP 8.2
- •Laravel 10
- •MySQL 8 via Docker (for full parity with production)
- •PHPUnit 10
For local dev, I use my Docker setup that mirrors staging. Saves time swapping DB drivers. Most UAE clients want bilingual apps, so I run tests with both Arabic and English headers, especially for validation messages.
I’ll skip the "test-driven development" dogma. On tight client timelines, I often write tests after the initial build phase. But I’ll go back and add them for edge cases that keep popping up — like when that delivery app’s date format broke because a dev used now() without a timezone.
Planning the Test Cases
I don’t write tests for everything. That’s not efficient. Focus areas:
- •Auth flows (Sanctum/Passport)
- •Validation rules with custom messages
- •Business logic hooks in controllers
- •Webhooks (especially for payment integrations)
For a real estate platform I built in Abu Dhabi, I wrote 9 test scenarios just for their property search. They wanted bilingual filters (Arabic/English) and custom field validation for local property codes. The tests caught several false negatives when the Arabic locale misencoded the query strings.
I document test plans directly in Laravel’s Feature tests. For example:
test('property search returns correct results for Arabic query')
test('property code filter rejects invalid UAE format')Clients in Dubai often need audit trails. I’ll test logging hooks at the repository layer, even if the feature isn’t exposed in the UI.
Writing Tests with PHPUnit
My tests look like this:
public function test_delivery_route_requires_valid_zip_code()
{
$response = $this->postJson('/api/route', [
'zip_code' => 'invalid',
]);
$response->assertStatus(422)
->assertJsonFragment([
'zip_code' => ['The zip code must be a valid UAE format']
]);
}I’m strict about JSON structure in assertions. The assertJsonStructure() method saved me during a Tawasul Limo booking flow rework — we changed the response shape twice, and the tests flagged it instantly.
For endpoints with multiple roles, I test all combinations:
test('admin can delete routes but driver cannot', function ($userType, $expectedStatus) {
$user = User::factory()->create(['type' => $userType]);
$this->actingAs($user, 'api')
->deleteJson("/api/route/1")
->assertStatus($expectedStatus);
})->with([
['admin', 200],
['driver', 403],
]);These parameterized tests cut the test file size by 40% on a project with 15 user roles last year.
Running Tests and CI/CD Integration
Local testing isn’t enough. I run 3 separate tests:
- Local:
./vendor/bin/phpunit(before every commit) - Git hook: Staging runs Laravel's tests and PHPStan level 7
- Production: Smoke tests for critical endpoints
I use GitHub Actions for CI with a matrix that tests:
- •PHP 8.1 and 8.2
- •MySQL 8 and MariaDB
- •Both environments with queue workers enabled
One project had weird issues with failed jobs in production queues. I added a test that spins up Redis in the CI Docker container. Took 45 extra minutes to configure, but it caught two major bugs before next client demo.
Debugging Failed Tests
When tests fail in CI but pass locally, I check:
- •Environment variables (e.g.,
APP_URLwaslocalhostinstead ofapi.example.com) - •Database collation (
utf8mb4vsutf8) - •Middleware order (especially for CORS and rate limiting)
Last month, an API test failed on the first request because the staging .env file used MYSQLI instead of PDO driver. Tests passed locally since we use Docker with MySQL. I added explicit DB checks in the CI setup — a 10-minute fix that saved 3 hours of future debugging.
Real-World Gotcha: Testing Localization Edge Cases
While working on Tawasul Limo’s multilingual validation, I realized Laravel’s :input attribute doesn’t handle Arabic well in error messages. For example:
'price' => 'The :input field is required'When the input name was in Arabic, it returned empty strings in error messages. My test caught this, leading me to switch to :attribute throughout and write a custom formatter that properly encodes non-Latin input names.
This type of issue makes up 30% of my Laravel API test fixes for UAE-based apps — especially when clients want Arabic validation messages with dynamic attributes.
Frequently Asked Questions
How do I test file uploads in Laravel APIs?
Use Laravel’s UploadedFile::fake() to generate dummy files. For actual validation (like max 10MB or specific formats), write tests that simulate uploads with different sizes and extensions. I always include a test that checks for proper cleanup in failed requests — a common pain point in multitenant apps.
Should I use Pest or PHPUnit for Laravel API tests?
PHPUnit comes with Laravel out of the box. For most projects, it’s sufficient. I tried Pest for a personal project but didn’t love the extra syntax. Stick to whatever your team already knows — framework choice matters less than consistent test coverage.
How do I test API rate limiting?
Laravel’s ThrottlesRequests trait makes this easy. I create a test that sends 11 requests to the same endpoint within a second. Make sure to use assertResponseStatus(429) and check response headers. For UAE clients handling flash sales, I double the test frequency (25 requests per second, etc.).
What’s the fastest way to seed test data?
I use Laravel’s seed files instead of migrations during tests — faster and avoids database conflicts. For complex relationships, I created two sets of factories: minimal for simple tests, and fully-loaded for edge cases. Don’t forget to use ::withoutEvents() if the model triggers listeners you don't need in tests.
Tired of fixing bugs in production? I’ve got your back. Whether you’re building a bilingual API for a Dubai startup or scaling a Laravel app for Abu Dhabi enterprise — I've been there. Book a free consultation to walk through your testing setup, or send me a message if you’re struggling with specific Laravel issues.