Essential Laravel Testing: A Complete Overview


Because bugs are just features we forgot to test.
In modern Laravel development, testing is no longer a luxury. It’s a necessity. This article walks you through the most essential types of tests you can (and should) implement in your Laravel 12 application, powered by PHP 8.4.
🧪 1. Unit Tests
Goal: Test a class or a method in isolation.
Location: tests/Unit
Example:
use App\Services\VatCalculator;
test('it calculates the VAT correctly', function (): void {
$calculator = new VatCalculator();
$result = $calculator->calculate(100.0, 20.0);
expect($result)->toBe(120.0);
});
Best practice: Use clear method names like it_calculates_vat_correctly
, and avoid dependencies (e.g., database).
🧩 2. Feature Tests
Goal: Test the integration of multiple components: routes, controllers, views, database.
Location: tests/Feature
Example:
test('user can create a blog post', function (): void {
$user = User::factory()->create();
$this->actingAs($user)
->post('/posts', [
'title' => 'Hello World',
'content' => 'My first blog post',
])
->assertRedirect('/posts');
});
Tip: Use traits like RefreshDatabase
to isolate each test run.
🌐 3. HTTP / API Tests
Goal: Simulate RESTful API calls and assert response structure and status.
Example:
test('API returns paginated users', function (): void {
User::factory()->count(30)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [['id', 'name', 'email']],
'links', 'meta',
]);
});
Useful for: Auth headers, error formats, validation, pagination.
🔗 4. Integration Tests
Goal: Verify collaboration between multiple components (e.g., controller + service + DB).
Often confused with Feature Tests, but here you want to make sure the wiring between classes works as expected.
🎮 5. End-to-End (E2E) / Browser Tests
Tool: Laravel Dusk
Goal: Simulate full browser interactions from the user’s point of view.
Example:
$this->browse(function (Browser $browser): void {
$browser->visit('/login')
->type('email', 'john@example.com')
->type('password', 'secret')
->press('Login')
->assertPathIs('/dashboard');
});
Downside: Slower. Needs real or headless browsers.
🏛️ 6. Architecture / Contract Tests
Goal: Enforce architectural rules across your codebase.
Tool: spatie/pest-plugin-architecture
Example:
uses(Spatie\Architecture\Expectations\ArchTest::class);
it('Controllers should only use Services or Requests')
->expect('App\Http\Controllers')
->toUse('App\Services')
->ignoring('Illuminate\Http\Request');
Why: Maintain clear boundaries and avoid spaghetti code.
📸 7. Snapshot Tests
Goal: Ensure that output (JSON, HTML, etc.) does not change unexpectedly.
Tool: spatie/phpunit-snapshot-assertions
Example:
test('returns expected JSON structure', function (): void {
$response = $this->getJson('/api/posts');
$response->assertMatchesJsonSnapshot();
});
🔁 8. Test-Driven Development (TDD)
Cycle: Red -> Green -> Refactor
Write a failing test
Make it pass
Refactor
Example:
test('calculates discount for VIP', function (): void {
$service = new DiscountService();
$result = $service->calculate(100.0, 'vip');
expect($result)->toBe(90.0); // 10% off
});
TDD helps you write only the code you need—no more, no less.
⚗️ 9. Bonus: Mutation, Fuzzing & Performance Tests
Mutation Testing: Use Infection to verify if your tests detect common bugs.
Fuzzing: Throw random inputs at your functions to test stability.
Performance: Benchmark long-running processes or APIs.
✅ Conclusion
You don’t have to write all the tests—but you should aim for the ones that deliver maximum confidence with minimum maintenance. At a minimum:
Use Unit tests for logic
Feature tests for user flows
API tests for endpoints
Architecture tests for boundaries
Want confidence in your code? Test it like you mean it.
Happy testing, artisan!
Subscribe to my newsletter
Read articles from Jean-Marc Strauven directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Jean-Marc Strauven
Jean-Marc Strauven
Jean-Marc (aka Grazulex) is a developer with over 30 years of experience, driven by a passion for learning and exploring new technologies. While PHP is his daily companion, he also enjoys diving into Python, Perl, and even Rust when the mood strikes. Jean-Marc thrives on curiosity, code, and the occasional semicolon. Always eager to evolve, he blends decades of experience with a constant hunger for innovation.