Laravel Testing with PHPUnit and PEST: A Complete Beginner Guide

Arlind Musliu cofounder at Lucky Media
Arlind Musliu

January 12, 2024 · 6 min read

Laravel for Beginners: PHPUnit and PEST Tests

2026 UPDATE - LARAVEL 13

We’re excited to announce that we have updated all of our blog post examples to reflect the new Laravel 13 version! Our previous examples were based on Laravel 10, but with the release of Laravel 13, we wanted to ensure that our readers have access to the most up-to-date information and examples.

Testing Your Laravel Application

Testing is a crucial step in ensuring that your application works as expected and remains stable over time. Laravel, being a framework that embraces testing, provides excellent support for writing tests with PHPUnit, and has also seen growing support for PEST, a testing framework with a focus on simplicity and elegance. In this article, we’ll explore how you can use PHPUnit and PEST to write tests for your Laravel app.

PEST: A Fresh Approach to Testing in Laravel

PEST is a testing framework that works on top of PHPUnit, offering a different approach with a focus on simplicity and a clean syntax. PEST allows you to write tests more expressively and elegantly.

Getting Started with PEST

To start using PEST in your Laravel blog, you’ll need to create the test with the --pest flag:

php artisan make:test UserTest --pest

Writing Your First PEST Test

After installing PEST, you can write more concise tests. Here’s the same test we wrote with PHPUnit, now with PEST:

namespace Tests\Feature;

use Tests\TestCase;

class PostTest extends TestCase
{
	it('can access posts index page', function () {
		$response = $this->get('/posts');

		$response->assertStatus(200);
	});
}

To run your PEST tests, you can use the same commands as for PHPUnit tests:

php artisan test

Using PEST Features

PEST supports all PHPUnit assertions, but it also provides additional features and helpers that make testing more enjoyable. For example, you can use higher-order tests to reduce boilerplate:

namespace Tests\Feature;

use Tests\TestCase;

class PostTest extends TestCase
{
	it('can access posts index page')->get('/posts')->assertStatus(200);
}
Using Laravel Pipelines

PHPUnit: The Foundation of Testing in Laravel

PHPUnit was the de facto standard for unit testing PHP applications. Laravel is built with testing in mind, and it includes out-of-the-box support for PHPUnit with a phpunit.xml configuration file and some base test classes.

Writing Your First PHPUnit Test

When you install Laravel, it comes with an example test file. To write a new test, you can create a file within the tests/Feature or tests/Unit directory, depending on the type of test you’re writing. Here’s an example of a feature test that checks if the blog posts index page is accessible:

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function posts_index_page_can_be_accessed()
    {
        $response = $this->get('/posts');

        $response->assertStatus(200);
    }
}

Or we can simplify it with this:

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function posts_index_page_can_be_accessed()
    {
		$this->get('/posts')->assertStatus(200);
    }
}

You can run your tests using the following command:

php artisan test

Testing Database Interactions

Laravel provides traits like RefreshDatabase that you can use within your tests to reset your database after each test. This ensures that your tests are independent and won’t affect each other.

Here’s a test that ensures a new post can be created:

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/** @test */
public function a_new_post_can_be_created()
{
	$user = User::factory()->create(); // First we create a user

    $postData = [
        'title' => 'A New Post',
        'content' => 'Content of the new post',
    ];

	// This stimulates authentication when creating the new post
	$this->actingAs($user)
		 ->post('/posts', $postData);

    $this->assertDatabaseHas('posts', [
		 'user_id' => $user->id,
		 'title' => 'A New Post',
		 'content' => 'Content of the new post',
	]);
}
laravel best framework php

Feature Tests vs Unit Tests: When to Use Each

Laravel organizes tests into two directories:tests/Feature and tests/Unit. Understanding the difference shapes how you structure your entire test suite.

  • Unit tests test isolated logic with no database, no HTTP requests, and no framework bootstrapping. They are fast and granular, ideal for testing a single method or helper function in total isolation.

  • Feature tests test your application through the full request/response cycle, including routes, controllers, middleware, and the database. They confirm that your application actually behaves correctly end-to-end.

Rule of thumb for Laravel: Write mostly Feature tests. Laravel is a full-stack framework and the real value comes from testing how your routes, controllers, and models work together. Unit tests shine for standalone service classes, complex business logic calculations, or value objects that have no framework dependencies.

HTTP Testing: Testing Routes and Controllers

Laravel’s HTTP testing helpers let you simulate real HTTP requests against your application without a running browser or server. Laravel boots the full application internally and returns a test response you can inspect with assertions.

Testing GET Requests

// PEST syntax
it('shows the posts index page', function () {
    $response = $this->get('/posts');

    $response->assertStatus(200);
    $response->assertSee('All Posts');
});

// PHPUnit syntax
public function test_posts_index_page_is_accessible(): void
{
    $response = $this->get('/posts');

    $response->assertStatus(200);
    $response->assertSee('All Posts');
}

Testing POST Requests

Use $this->post() to simulate form submissions. Pair it with database assertions to verify both the HTTP response and the resulting database state:

it('creates a new post', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)->post('/posts', [
        'title' => 'My First Post',
        'content' => 'Some content here',
    ]);

    $response->assertRedirect('/posts');

    $this->assertDatabaseHas('posts', [
        'title' => 'My First Post',
        'user_id' => $user->id,
    ]);
});

Testing Authenticated Routes with actingAs()

The actingAs() helper is one of the most-used methods in all of Laravel testing. It authenticates a user for the duration of a single test, so you can test protected routes without building a full login flow. Here is how to also verify that unauthenticated requests are correctly rejected:

it('requires authentication to create a post', function () {
    // Unauthenticated request should redirect to login
    $this->post('/posts', ['title' => 'Test'])
         ->assertRedirect('/login');
});

it('allows an authenticated user to create a post', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
         ->post('/posts', [
             'title' => 'Test Post',
             'content' => 'Some content',
         ])
         ->assertRedirect('/posts');
});

Using Model Factories for Test Data

Model factories generate fake but realistic database records for your tests. They ship with every Laravel application and live in database/factories. Without factories, database testing becomes tedious - you would need to manually insert raw SQL or use slow database seeders.

// Create and persist a user to the database
$user = User::factory()->create();

// Create without persisting (useful in unit tests)
$user = User::factory()->make();

// Create 5 posts belonging to a specific user
$posts = Post::factory()->count(5)->for($user)->create();

// Use a factory state (defined in your factory class)
$admin = User::factory()->admin()->create();

// Override specific attributes
$user = User::factory()->create(['email' => 'test@example.com']);

// Create a user with a related post in one call
$user = User::factory()->has(Post::factory()->count(3))->create();

The RefreshDatabase trait resets the database between each test so factory-created records never bleed between tests. For larger test suites, LazilyRefreshDatabase is a faster alternative - it only runs migrations when a test actually touches the database, skipping the reset for tests that do not need it.

PEST vs PHPUnit: The Same Test, Two Syntaxes

PEST v3 ships as the default testing framework in new Laravel 11+ projects. Both frameworks are fully supported - PEST runs on top of PHPUnit internally. Here is the same Feature test written in both so you can compare the syntax directly:

// PHPUnit - class-based syntax
namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_authenticated_user_can_create_a_post(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)
             ->post('/posts', [
                 'title' => 'My Post',
                 'content' => 'Post content',
             ])
             ->assertRedirect('/posts');

        $this->assertDatabaseHas('posts', ['title' => 'My Post']);
    }
}
// PEST v3 - function-based syntax
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(Tests\TestCase::class, RefreshDatabase::class);

it('allows an authenticated user to create a post', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
         ->post('/posts', [
             'title' => 'My Post',
             'content' => 'Post content',
         ])
         ->assertRedirect('/posts');

    $this->assertDatabaseHas('posts', ['title' => 'My Post']);
});

Choose PEST for new Laravel projects or when you prefer expressive, readable syntax. PEST v3 also adds architectural testing with arch() and mutation testing.

Stick with PHPUnit for existing codebases with established PHPUnit test suites, or when your team prefers class-based OOP style testing with maximum IDE tooling support.

Essential Laravel Test Assertions

These assertions cover the vast majority of what you will write in daily Laravel testing. All of them work identically in both PEST and PHPUnit:

// HTTP Response Assertions
$response->assertStatus(200);           // specific HTTP status code
$response->assertOk();                  // shorthand for assertStatus(200)
$response->assertNotFound();            // 404
$response->assertForbidden();           // 403
$response->assertRedirect('/path');     // response was a redirect
$response->assertSee('text');           // text appears in response HTML
$response->assertDontSee('text');       // text does not appear
$response->assertJson(['key' => 'val']); // response contains JSON data
$response->assertSessionHas('success'); // session has a flash key

// Database Assertions
$this->assertDatabaseHas('posts', ['title' => 'My Post']);
$this->assertDatabaseMissing('posts', ['title' => 'Deleted Post']);
$this->assertDatabaseCount('posts', 5);
$this->assertSoftDeleted('posts', ['id' => 1]);

Running and Filtering Tests

# Run all tests
php artisan test

# Run only a specific test class
php artisan test --filter PostTest

# Run a single test by its description
php artisan test --filter "creates a new post"

# Stop immediately on the first failure
php artisan test --bail

# Run tests in parallel (significantly faster for large suites)
php artisan test --parallel

# Generate a code coverage report (requires Xdebug or PCOV)
php artisan test --coverage

For shared setup across tests within a PEST file, use beforeEach() instead of duplicating User::factory()->create() in every test:

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function () {
    $this->user = User::factory()->create();
});

it('can view all posts', function () {
    $this->actingAs($this->user)
         ->get('/posts')
         ->assertOk();
});

it('can create a post', function () {
    $this->actingAs($this->user)
         ->post('/posts', ['title' => 'New Post', 'content' => 'Content'])
         ->assertRedirect('/posts');
});

Conclusion

Testing is an integral part of the development cycle, and Laravel provides robust tools to help you ensure that your blog functions correctly. Whether you prefer the familiarity of PHPUnit or the expressive syntax of PEST, Laravel has got you covered.

With PHPUnit, you have a powerful and well-established testing framework at your disposal. If you’re looking for a more modern and elegant approach, PEST is an excellent choice that can make your tests even more readable and enjoyable to write.

Upcoming Articles in the Series

  1. Laravel for Beginners: Blade and Breeze

  2. Laravel for Beginners: Service Providers

  3. Laravel for Beginners: Form Requests and Validation Rules

This article is part of our series Laravel for Beginners: A Step-by-Step Guide to Learn the Concepts.


Bring Your Ideas to Life 🚀

If you need help with a Laravel project let’s get in touch.

Lucky Media is proud to be recognized as a leading Laravel Development Agency

Technologies

Laravel
Arlind Musliu cofounder at Lucky Media
Arlind Musliu

Cofounder and CFO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork