Automated Testing in Laravel: PHPUnit and Pest
Master automated testing in Laravel with PHPUnit and Pest framework, including unit tests, feature tests, and test-driven development practices.
StalkTechie
Author
Automated Testing in Laravel: PHPUnit and Pest
Testing isn't just a checkbox-it's your app's safety net, catching bugs before users do and letting you refactor with confidence. Laravel ships with PHPUnit out of the box, but Pest adds a modern, expressive twist. This guide covers setup, writing unit/feature tests, TDD workflows, and tips to make testing a habit. Whether you're on Laravel 11.x in 2025 or earlier, think of tests as documentation that runs itself. Let's make your code bulletproof without the boredom.
Setup and Basics
Laravel includes PHPUnit via Composer. Run tests with php artisan test (or vendor/bin/phpunit).
# Fresh app has tests in /tests directory
# Unit: Isolated logic
# Feature: Full stack (HTTP, DB)
Config in phpunit.xml: Environments, DB (use sqlite :memory: for speed). Install Pest for cleaner syntax:
composer require pestphp/pest --dev
php artisan pest:install # Adds Pest.php, converts tests
Pest uses closures over classes-more readable. Fallback to PHPUnit if needed. Human tip: Start small; aim for 70% coverage on critical paths like auth/payments. Tools like Laravel Dusk for browser tests if UI-heavy.
Writing Unit Tests: Isolated Logic
Test pure functions, models, services without DB/HTTP.
// tests/Unit/ExampleTest.php (PHPUnit style)
public function test_basic_math()
{
$this->assertEquals(4, 2 + 2);
}
// With Pest (tests/ExampleTest.php)
it('adds numbers correctly', function () {
expect(2 + 2)->toBe(4);
});
Real example: Test a service.
// app/Services/Calculator.php
class Calculator {
public function add($a, $b) { return $a + $b; }
}
// Test
uses()->beforeEach(function () {
$this->calc = new Calculator();
});
it('adds positive numbers', function () {
expect($this->calc->add(5, 3))->toBe(8);
});
it('handles negatives', function () {
expect($this->calc->add(-2, 3))->toBe(1);
});
Mock dependencies with Mockery (included): $mock = Mockery::mock(Service::class)->shouldReceive('method')->andReturn('fake');.
Feature Tests: End-to-End Flows
Hit routes, assert responses, DB state.
// tests/Feature/AuthTest.php (Pest)
beforeEach(function () {
$this->user = User::factory()->create();
});
it('logs in user', function () {
$response = $this->post('/login', [
'email' => $this->user->email,
'password' => 'password',
]);
$response->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($this->user);
});
it('registers new user', function () {
$response = $this->post('/register', [
'name' => 'Ali',
'email' => 'ali@example.com',
'password' => 'secret123',
'password_confirmation' => 'secret123',
]);
$response->assertRedirect('/home');
$this->assertDatabaseHas('users', ['email' => 'ali@example.com']);
});
Use factories: User::factory(10)->create();. Refresh DB: $this->refreshDatabase(); trait for migrations per test.
HTTP assertions: assertStatus(200), assertJson(['key' => 'value']). For APIs: actingAs($user, 'api').
Test-Driven Development (TDD) Practices
Red-Green-Refactor: Write failing test first, code to pass, improve.
- Test:
it('calculates tax', ...);-fails. - Implement minimal code in service.
- Run tests, refactor.
Example cycle for a controller:
// First: Test store method fails validation
it('validates product creation', function () {
$response = $this->post('/products', []);
$response->assertSessionHasErrors(['name']);
});
// Then add validation in controller...
Benefits: Cleaner design, fewer bugs. Tools: Pest's datasets for parameterized tests dataset('numbers', [[1,2,3], ...]).
Advanced Tips and Coverage
- Mocking Externals: HTTP clients with
Http::fake();-assert sent requests. Mail::fake();,Notification::fake();for emails/notifs.- Events/Queues:
Event::fake();, test dispatched. - DB Transactions: Tests rollback automatically with DatabaseTransactions trait.
- Coverage:
php artisan test --coverage-html reports(with Xdebug). Aim high on business logic. - CI/CD: GitHub Actions: Run tests on push, fail PRs on low coverage.
- Pest Plugins: pest-plugin-laravel for expectations like
expect($response)->toBeSuccessful().
Pitfalls: Don't test Laravel core (e.g., route exists)-focus on your logic. Slow tests? Use in-memory SQLite.
Debugging and Best Practices
Fail fast: --stop-on-failure. Parallelize: php artisan test --parallel (Laravel 9+). Real talk: Tests save hours-write them during dev, not after. Pair with PhpStorm for breakpoints. Community faves: Jeffrey Way's Laracasts for deep dives.
Embrace testing in Laravel-PHPUnit for structure, Pest for joy. Your future self (and team) will thank you when deploys are fearless. Start with one failing feature test today!