Skip to main content
Tutorial

Testing Laravel APIs: My Actual Workflow on Real Projects

5 min read

Sarah shares her actual Laravel API testing workflow, with real-world examples and tools tested in UAE projects.

Laravel testingAPI developmentPHPUAE developersPHPUnit

Last month, I deployed a booking API for a logistics client in Dubai that handles 10,000+ daily requests. Three hours before launch, a test caught a pagination bug that would’ve returned empty responses for Arabic-formatted date queries. That moment reminded me why my Laravel API testing workflow has saved my ass more times than I can count.

Starting Point: The Test Environment Setup That Doesn’t Waste Time

I use Laravel’s built-in PHPUnit integration but tweak the defaults aggressively. Most tutorials tell you to stick with SQLite for speed – they’re wrong if you care about database consistency. For client projects in the UAE, I always swap to PostgreSQL (yes, even in testing – Neon Postgres handles this cleanly) to catch type-related bugs early.

bash
# In phpunit.xml
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_DATABASE" value="test_db"/>

Once, a payment gateway integration passed tests with SQLite because it ignored timestamp precision issues. When it hit staging’s Postgres, we found timestamps were getting truncated to seconds instead of milliseconds. The client was furious – and honestly, they should’ve been. That’s when I hardened this rule: test databases must mirror production exactly.

Writing Tests: Not Just for Beginners

At my last contract with a real estate platform in Abu Dhabi, I wrote 150+ tests over six weeks. Here’s how I structure them:

  1. Smoke tests: Basic route existence checks
  2. Input validation: Field requirements, length limits, format checks
  3. Behavior tests: State changes after requests
  4. Edge cases: SQL injection attempts, unexpected headers, missing tokens

For example, a password reset endpoint I built for Tawasul Limo had:

php
public function test_invalid_token_fails()
{
    $response = $this->post('/reset-password', [
        'token' => 'invalid',
        'email' => 'user@example.com',
        'password' => 'newPass123'
    ]);
    
    $response->assertStatus(422);
    $this->assertDatabaseMissing('users', ['email' => 'user@example.com']);
}

I used to think this was overkill until I caught a bug where expired tokens were resetting accounts without checking expiration. Now I write tests for every conditional branch – even the ones I’m “sure” can’t fail.

Authentication: Testing Protected Endpoints Without Headaches

For API authentication, I test both bearer tokens and cookie sessions separately. One healthcare app I built required JWT refresh tokens working across both mobile and web – it was a nightmare to test until I structured it this way:

php
public function test_jwt_revocation_on_logout()
{
    // First get valid token
    $login = $this->post('/login', ['email' => 'test@local']);
    $token = $login['token'];
    
    // Revoke token via logout
    $this->withHeaders([
        'Authorization' => 'Bearer '.$token
    ])->post('/logout');
    
    // Ensure token is revoked
    $this->withHeaders([
        'Authorization' => 'Bearer '.$token
    ])->get('/user-profile')->assertStatus(401);
}

Last year, I spent two days debugging why logout failed in the UAE app store version of Greeny Corner. Turns out the refresh token revocation wasn't being cleared from local storage during the test. Now I always test mobile-specific headers and storage behaviors separately.

Handling Failures That Slap You in the Face

Automated tests don’t catch everything – don’t kid yourself. Two years ago, a client’s production logs showed 500 errors when users submitted extra-long phone numbers (looking at you, Saudi dial codes). My tests had checked length restrictions but forgot about PHP’s input sanitization quirks. That day I learned:

  • Always test inputs at minimum/maximum +1 boundaries
  • Run some tests directly against staging (I use neon.tech for ephemeral test DBs)
  • Check Laravel’s actual logs – not just test output

Integration into CI/CD: The Final Gatekeeper

My GitHub Actions workflow runs four testing jobs:

  1. PHPStan analysis
  2. PHPUnit test suite
  3. Postman collection via Newman (for contract testing)
  4. Load testing with k6 (for critical endpoints)

This pipeline caught a query performance degradation in Reach Home Properties' API that saved the client over AED 20,000/year in server costs. You can see the full pipeline here.

Frequently Asked Questions

### What PHPUnit version works best with Laravel 10?

Laravel 10 requires PHPUnit 9.5+. I lock mine to 9.5.28 – newer versions have issues with Laravel’s testing helpers. PHP 8.1 compatibility is solid at this combination.

### How do you test rate-limited API routes?

Use the throttle middleware in test environment config, then write a test that hits the endpoint past the limit. I always check both the 429 status code and the Retry-After header value.

### Why aren't my session-based test assertions working?

Ensure you're using StartsApplication trait and testing through HTTP calls. Session assertions like $this->assertSessionHas() won’t work if you're mocking requests directly.

### How do you test file uploads effectively?

Save test files in storage/app/testing and use Laravel’s UploadedFile::fake() in memory. For production-like validation, I occasionally run specific tests against real file storage drivers – not just mocks.

I’ve been building and testing Laravel APIs since 2017 across UAE and GCC projects – from ride-hailing apps to enterprise billing systems. If you’re looking for real-world testing strategies that hold up in production, let’s talk. You can book a free 30-minute consultation to walk through your specific testing challenges.

S

Sarah

Senior Full-Stack Developer & PMP-Certified Project Lead — Abu Dhabi, UAE

7+ years building web applications for UAE & GCC businesses. Specialising in Laravel, Next.js, and Arabic RTL development.

Work with Sarah