Laravel's testing tools make full-stack tests fast to write, which is what keeps large apps shippable.
Laravel apps ship ready for testing with Pest or PHPUnit (you choose when creating the app). Run php artisan test. Feature tests in tests/Feature boot the whole app and make simulated HTTP requests; unit tests in tests/Unit test classes in isolation. phpunit.xml points tests at an in-memory SQLite database by default.
Key tools:
use RefreshDatabase;migrates once and wraps each test in a transaction that's rolled back, so every test starts clean.- Factories create realistic models:
User::factory()->create(),Post::factory()->count(3)->for($user)->create(), with states like->unverified(). - HTTP helpers:
$this->get('/posts'),post,put,delete, and JSON variantspostJsonandgetJson. - Assertions on the response:
assertOk(),assertStatus(201),assertRedirect(),assertSee('Title'),assertJsonPath('data.0.id', 1),assertInvalid(['title']),assertForbidden(). actingAs($user)authenticates a request.- Database assertions:
assertDatabaseHas('posts', [...]),assertDatabaseCount,assertModelMissing.
Fakes replace side effects with recorders: Queue::fake() then Queue::assertPushed(SendInvoice::class); Mail::fake(), Notification::fake(), Event::fake(), Storage::fake('s3'), Http::fake([...]) for outgoing HTTP calls. $this->freezeTime() or travel(5)->days() control the clock.
Test behaviour through HTTP endpoints for most features, and keep unit tests for complex domain logic.
Example
<?php
// tests/Feature/PublishPostTest.php
use App\Jobs\NotifySubscribers;
use App\Models\{Post, User};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
class PublishPostTest extends \Tests\TestCase
{
use RefreshDatabase;
public function test_author_can_publish_and_subscribers_are_notified(): void
{
Queue::fake();
$author = User::factory()->create();
$post = Post::factory()->for($author, 'author')->create(['status' => 'draft']);
$this->actingAs($author)
->putJson("/api/v1/posts/{$post->id}/publish")
->assertOk()
->assertJsonPath('data.status', 'published');
$this->assertDatabaseHas('posts', ['id' => $post->id, 'status' => 'published']);
Queue::assertPushed(NotifySubscribers::class, fn ($job) => $job->post->is($post));
}
// + a test that another user gets ->assertForbidden()
}Remember
- RefreshDatabase plus factories give each test a clean, realistic database
- HTTP test helpers and actingAs exercise routes end to end
- Queue, Mail, Event, Http and Storage fakes record side effects for assertions
Watch out for
- Hitting real external APIs in tests instead of Http::fake(), making the suite slow and flaky.