Writing Laravel tests is the single habit that turns a project from a pile of code that merely “seems to work” into a codebase you can change with confidence. Tests tell you instantly which behaviour a refactor broke; they act as living documentation for the next developer; and most importantly, they remove the “did I break something?” anxiety from every deploy. In this guide we'll cover Laravel's two test engines — the default PHPUnit and the increasingly popular Pest with its cleaner syntax — through practical examples.
How testing is set up in Laravel
A fresh Laravel project already ships with testing. The phpunit.xml file at the root defines the test environment, while the tests/ folder splits into two directories: tests/Unit and tests/Feature. The distinction is conceptual:
- Unit tests exercise a single class or method in isolation, without booting the full framework. They are fast and focus on pure logic (calculations, formatting, small services).
- Feature tests verify an HTTP request, a route, the database and several classes working together, end to end. These usually deliver the most real-world value.
The most practical way to run them is Laravel's own wrapper:
php artisan test
# or directly:
./vendor/bin/phpunit
# to filter a single test:
php artisan test --filter=ProjectCanBeCreated
In phpunit.xml the test environment is typically configured with APP_ENV=testing and an in-memory SQLite database (DB_CONNECTION=sqlite, DB_DATABASE=:memory:). That way tests run against a clean schema every time, without touching your real database.
Writing your first feature test
Artisan is enough to scaffold a test class:
php artisan make:test ProjectTest # tests/Feature
php artisan make:test PriceTest --unit # tests/Unit
A simple feature test starts by confirming that a page loads correctly. With PHPUnit syntax:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProjectTest extends TestCase
{
use RefreshDatabase;
public function test_homepage_loads_successfully(): void
{
$response = $this->get('/');
$response->assertStatus(200);
$response->assertSee('Aslain');
}
}
Here $this->get('/') simulates a real HTTP request; assertStatus and assertSee verify the response. The test method name must start with test_ (or be marked with the #[Test] attribute) for PHPUnit to recognise it.
Database tests and RefreshDatabase
For tests that touch the database, the RefreshDatabase trait is essential: it resets the schema inside a transaction before each test and rolls it back afterwards, so tests never bleed into each other. To produce data you use factories:
public function test_project_is_persisted(): void
{
$project = Project::factory()->create([
'title' => ['en' => 'Test Project'],
]);
$this->assertDatabaseHas('projects', [
'id' => $project->id,
]);
}
assertDatabaseHas queries a row directly. Its counterpart assertDatabaseMissing is handy when testing deletions. Factories live under database/factories and generate realistic random data via the fake() helper.
Authentication, JSON and form submissions
In real applications most routes require an authenticated user. Laravel handles this in a single line with actingAs:
public function test_authorised_user_can_add_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/admin/projects', [
'title_en' => 'New Project',
]);
$response->assertRedirect('/admin/projects');
$this->assertDatabaseHas('projects', ['title->en' => 'New Project']);
}
For JSON-based APIs there are methods like getJson and postJson, plus structure-aware assertions such as assertJson / assertJsonStructure:
$this->getJson('/api/projects')
->assertOk()
->assertJsonCount(3, 'data')
->assertJsonStructure(['data' => [['id', 'title']]]);
Writing the same tests more concisely with Pest
Pest is a testing framework built on top of PHPUnit; the engine is identical, the syntax is more fluent. Instead of classes and methods you write function-based tests. The same feature test looks like this in Pest:
<?php
use App\Models\User;
it('lets an authorised user add a project', function () {
$user = User::factory()->create();
$this->actingAs($user)
->post('/admin/projects', ['title_en' => 'New Project'])
->assertRedirect('/admin/projects');
expect(Project::count())->toBe(1);
});
The expect() expression provides a readable assertion chain (toBe, toBeTrue, toContain…). Traits like RefreshDatabase are applied to a whole directory at once via uses() in the tests/Pest.php file. To set Pest up, add the pestphp/pest package with Composer and run php artisan pest:install; your existing PHPUnit tests keep running side by side untouched.
Faking dependencies: mocks and fakes
Good tests never really connect to external services (email, payment, notifications). Laravel ships ready-made fakes for this. For example, you can assert a notification was sent without dispatching a real email:
use Illuminate\Support\Facades\Mail;
Mail::fake();
// ... action ...
Mail::assertSent(WelcomeMail::class);
The same idea applies to Queue::fake(), Event::fake(), Storage::fake() and Http::fake(). These keep tests fast and deterministic — they no longer depend on a network connection or the state of a third-party service.
Frequently Asked Questions
Should I pick PHPUnit or Pest?
Both use the same engine, so it isn't a contest of technical superiority. If you want a class-based, familiar and enterprise-friendly structure, choose PHPUnit; if you prefer less boilerplate and readable syntax, Pest makes sense. For new projects Pest is increasingly becoming the default, but your team's habits should decide.
Should I test everything?
No. Aiming for 100% coverage is usually wasteful. Test the business-critical flows first (payment, registration, authorisation, orders) and the spots that have broken before. Writing tests for plain getters/setters or the framework's own code is wasted effort.
Why are my tests slow?
The most common reasons are hitting a real database instead of in-memory SQLite, and not faking external services. With :memory: SQLite, RefreshDatabase and proper fakes, a test suite can run in seconds.
Want to bake testing into your project? If you need help setting up a safe test suite on your existing Laravel app, all the way to CI/CD integration, get in touch — let's harden your codebase together.