Skip to main content
Laravel, shipping fast.
describe('CreateLicenseAction', function () {
    test('creates license via manager', function () {
        $action = new CreateLicenseAction();

        $license = $action([
            'name' => 'My Premium License',
            'domain' => 'example.com',
        ]);

        expect($license)
            ->toBeInstanceOf(\App\Http\DataObjects\LicenseDTO::class)
            ->toHaveProperty('key')
            ->toHaveProperty('name', 'My Premium License')
            ->toHaveProperty('domain', 'example.com')
            ->toHaveProperty('status', 'active')
            ->and($license->key)->toStartWith('lic_');
    });

    test('validates required fields', function () {
        $action = new CreateLicenseAction();

        expect(fn () => $action([
            'name' => 'Missing domain field',
        ]))
            ->toThrow(\Illuminate\Validation\ValidationException::class);
    });
});

Testing Middleware

describe('EnsureTokenIsValidMiddleware', function () {
    test('allows valid tokens', function () {
        $bearer = Bearer::factory()->create();

        $response = $this->getJson(
            '/api/licenses',
            headers: ['Authorization' => "Bearer {$bearer->token}"]
        );

        expect($response->status())->not()->toBe(401);
    });

    test('rejects missing token', function () {
        $response = $this->getJson('/api/licenses');

        $response->assertStatus(401);
    });

    test('rejects revoked tokens', function () {
        $bearer = Bearer::factory()->create(['revoked' => true]);

        $response = $this->getJson(
            '/api/licenses',
            headers: ['Authorization' => "Bearer {$bearer->token}"]
        );

        $response->assertStatus(401);
    });

    test('rejects expired tokens', function () {
        $bearer = Bearer::factory()->create([
            'expires_at' => now()->subDay(),
        ]);

        $response = $this->getJson(
            '/api/licenses',
            headers: ['Authorization' => "Bearer {$bearer->token}"]
        );

        $response->assertStatus(401);
    });
});

Testing Exception Handling

describe('Exception Handling', function () {
    test('validation exceptions return 422', function () {
        $bearer = Bearer::factory()->create();

        $response = $this->postJson(
            '/api/license',
            data: ['name' => ''], // Invalid - missing required field
            headers: ['Authorization' => "Bearer {$bearer->token}"]
        );

        $response->assertStatus(422);
    });

    test('unauthorized returns 401', function () {
        $response = $this->getJson('/api/licenses');

        $response->assertStatus(401)
            ->assertJsonPath('error', 'Unauthorized');
    });

    test('rate limit exceptions return 429', function () {
        $bearer = Bearer::factory()->create();

        // Make requests exceeding rate limit
        for ($i = 0; $i < 65; $i++) {
            $this->getJson(
                '/api/licenses',
                headers: ['Authorization' => "Bearer {$bearer->token}"]
            );
        }

        $response = $this->getJson(
            '/api/domains',
            headers: ['Authorization' => "Bearer {$bearer->token}"]
        );

        $response->assertStatus(429)
            ->assertJsonPath('code', 'RATE_LIMIT_EXCEEDED');
    });
});

Running Tests

# Run all tests
php artisan test

# Run specific file
php artisan test --filter=LicenseControllerTest

# Run with coverage
php artisan test --coverage

# Run and stop on first failure
php artisan test --stop-on-failure

# Run in parallel (faster)
php artisan test --parallel

Quality Checklist

  • ✅ PHPStan Level 9 (all errors eliminated)
  • ✅ Code formatting with Pint
  • ✅ >80% code coverage
  • ✅ Zero architectural violations
  • ✅ All endpoints tested
  • ✅ Error scenarios tested
  • ✅ Edge cases tested
  • ✅ Database transactions in tests
  • ✅ Proper mocking of external calls
  • ✅ Performance tests for slow endpoints

Chapter 7 Summary

Testing is not a burden. It's a superpower. Tests let you refactor confidently, catch bugs early, and deploy with confidence.

Test pyramid:

  1. Many unit tests (fast, isolated)
  2. Some integration/feature tests (realistic)
  3. Few end-to-end tests (slow but comprehensive)

What to test:

  • All business logic
  • All error scenarios
  • Edge cases and boundary conditions
  • API behavior (status codes, responses)

What not to test:

  • Laravel framework (it's already tested)
  • External libraries (assume they work)
  • Simple getters/setters

Write tests as you build. Make them fast. Keep them focused. Then you can refactor at will and deploy with confidence.