Laravel Testing with PHPUnit & Pest: Step-by-Step Guide

·

·

Automated tests protect Laravel applications from regressions during refactors and deployments. Laravel ships with PHPUnit configured and provides testing helpers for HTTP, database, events, and mail out of the box.

Project Test Setup

Tests live in the tests/ directory. phpunit.xml defines suites for Unit and Feature tests with environment overrides.

The APP_ENV=testing environment uses array cache and sqlite :memory: database by default for speed and isolation.

Step 1: Run the default test suite

Execute php artisan test or ./vendor/bin/phpunit. Laravel boots the application kernel for feature tests.

A passing example test confirms your environment is wired. Add tests alongside every new feature.

php artisan test
php artisan test --filter=ExampleTest

Step 2: Configure the testing database

In phpunit.xml or .env.testing set DB_CONNECTION=sqlite and DB_DATABASE=:memory: for fast in-memory tests.

For MySQL integration tests, use a dedicated testing database and RefreshDatabase trait to migrate before each test.

<env name="DB_DATABASE" value=":memory:"/>

Step 3: Install Pest optionally

Run composer require pestphp/pest –dev –with-all-dependencies then php artisan pest:install for expressive syntax.

Pest runs on PHPUnit under the hood. Existing PHPUnit tests continue to work alongside Pest files.

composer require pestphp/pest --dev
php artisan pest:install

Step 4: Use RefreshDatabase trait

Add use RefreshDatabase; to feature tests that touch the database. Laravel migrates fresh schema per test class or method.

Combine with DatabaseMigrations for slower but explicit migration runs when debugging schema issues.

use IlluminateFoundationTestingRefreshDatabase;

Feature and HTTP Tests

Feature tests hit routes end-to-end with $this->get(), $this->post(), and assertStatus(), assertRedirect(), assertJson().

Authenticate users with actingAs($user) to test authorization without manual session hacking.

Step 1: Test a GET route returns 200

Create tests/Feature/PostControllerTest.php. Call $response = $this->get('/posts') and $response->assertStatus(200).

Use assertSee() for Blade content or assertJson() for API responses.

$response = $this->get('/posts');
$response->assertStatus(200);

Step 2: POST with validation assertions

Submit invalid data with $this->post('/posts', []) and assertSessionHasErrors(['title']).

Valid submissions should assertRedirect() and assertDatabaseHas('posts', ['title' => 'Hello']).

$this->post('/posts', [])->assertSessionHasErrors('title');

Step 3: Authenticate with actingAs

Create a user with User::factory()->create() then $this->actingAs($user)->get('/dashboard').

Test forbidden access with assertForbidden() when a guest or wrong role hits protected routes.

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

Step 4: Use factories for test data

Model factories in database/factories generate realistic records. Post::factory()->count(10)->create() seeds list pages.

Define states like Post::factory()->published() for scenario-specific attributes.

Post::factory()->count(5)->create();

Mocking and Unit Tests

Unit tests isolate classes without booting the full HTTP stack. Mock external APIs and facades to keep tests fast and deterministic.

Laravel fakes for Event, Mail, Notification, Queue, and Storage prevent side effects during assertions.

Step 1: Fake the Mail facade

Call Mail::fake() before triggering an action. Assert Mail::assertSent(WelcomeEmail::class) afterward.

Use assertNothingSent() on negative paths where email should not fire.

Mail::fake();
// act...
Mail::assertSent(WelcomeEmail::class);

Step 2: Mock a service with Mockery

Bind a mock in the container: $this->mock(PaymentGateway::class, fn($m) => $m->shouldReceive('charge')->once());

Constructor injection resolves the mock automatically in the controller under test.

$this->mock(PaymentGateway::class, function ($mock) {
    $mock->shouldReceive('charge')->once()->andReturn(true);
});

Step 3: Write a Pest test with expectations

Pest tests use test('description', function() { … }) with expect($value)->toBeTrue() syntax.

Use beforeEach() for shared setup like actingAs or factory seeding across tests in a file.

test('guests cannot create posts', function () {
    $this->post('/posts', [])->assertRedirect('/login');
});

Step 4: Run tests in CI pipelines

Add php artisan test to GitHub Actions or GitLab CI with sqlite and cached composer dependencies.

Fail builds on test errors. Parallel testing with php artisan test –parallel speeds large suites.

php artisan test --parallel

Aadyanex builds production-ready Laravel applications with clean architecture, secure APIs, and long-term support.