$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[August 22, 2026] Laravel

Laravel Testing: PHPUnit and Pest

Laravel Testing: Tips & Tricks

────────────────────────────────────────────────────────

Laravel Testing: Tips & Tricks

Tip: Use assertDatabaseCount() Instead of count()

// Bad
$this->assertEquals(5, Post::count());

// Good
$this->assertDatabaseCount('posts', 5);

It's more readable and doesn't load models into memory.

Gotcha: RefreshDatabase is Slow

RefreshDatabase migrates the entire database for each test class. For large test suites, use DatabaseTransactions instead (but it doesn't work with queued jobs).

Tip: Test the Happy Path First

it('creates a post', function () {
    $response = $this->post('/posts', ['title' => 'Test']);
    $response->assertRedirect();
    $this->assertDatabaseHas('posts', ['title' => 'Test']);
});

Then test edge cases.

Tip: Use actingAs() for Auth Tests

$user = User::factory()->create();
$this->actingAs($user)->get('/dashboard')->assertOk();

Gotcha: Mocking External Services

Don't hit real APIs in tests. Use Http::fake():

Http::fake(['api.example.com/*' => Http::response(['ok' => true])]);

Tip: Test Email with Mail::fake()

Mail::fake();
$this->post('/register', ['email' => 'test@test.com']);
Mail::assertSent(WelcomeEmail::class);
Mail::assertSent(WelcomeEmail::class, fn($mail) => $mail->hasTo('test@test.com'));

Tip: Use assertSee() Carefully

// Checks HTML content
$response->assertSee('Hello');

// Checks exact text, ignoring HTML
$response->assertSee('Hello', false);

Gotcha: Time-Sensitive Tests

Use Carbon::setTestNow() for consistent time-based tests:

Carbon::setTestNow('2024-01-15');
// All `now()` calls return this date
Carbon::setTestNow(); // Reset

Tip: Use route:cache Carefully

php artisan route:cache is fast, but it doesn't work with closure-based routes. Every time you cache routes, Laravel serializes them. If you have Route::redirect() or closure callbacks, the cache breaks. Stick to controller-based routes in production.

Tip: Model APP_KEY Rotation

Rotating APP_KEY invalidates all encrypted data — cookies, encrypted DB columns, and password reset tokens. If you must rotate (e.g., after a leak), plan a migration that re-encrypts existing data with the new key.

Gotcha: Local Scope Leaks

Global scopes defined in booted() apply to ALL queries on that model — including relationships. An innocent User::all() in admin panel might exclude soft-deleted users if a global scope is active.

Senior Insight

After reviewing hundreds of PRs, the single quality signal I look for is whether tests cover the unhappy path. Most developers write the 'happy path' test and call it done. In production, it's the edge cases — database deadlocks, validation errors, expired tokens — that bring systems down. I've adopted mutation testing as a way to measure test quality, and it consistently reveals gaps that code coverage metrics miss.

Source: Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/laravel), Spatie Blog (https://spatie.be/blog)

────────────────────────────────────────────────────────
<-- back to posts