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:
- Many unit tests (fast, isolated)
- Some integration/feature tests (realistic)
- 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.