Home PHP Pest PHP: Modern Testing Framework for Laravel & PHP
Intermediate 3 min · July 13, 2026

Pest PHP: Modern Testing Framework for Laravel & PHP

Learn Pest PHP testing framework with practical examples, debugging tips, and real-world scenarios.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of PHP and Composer
  • Familiarity with PHPUnit (optional but helpful)
  • A PHP project (Laravel or plain PHP)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Pest PHP is a modern testing framework built on top of PHPUnit with a simpler syntax.
  • It uses functions like test() and it() instead of class methods.
  • Higher-order expectations and custom helpers reduce boilerplate.
  • Seamless integration with Laravel and other PHP projects.
  • Offers parallel testing, code coverage, and easy setup.
✦ Definition~90s read
What is Pest PHP?

Pest PHP is a modern testing framework that provides a clean, expressive syntax for writing tests in PHP, built on top of PHPUnit.

Think of Pest PHP as a streamlined recipe book for testing your code.
Plain-English First

Think of Pest PHP as a streamlined recipe book for testing your code. Instead of writing long, formal instructions (like PHPUnit), Pest gives you short, clear steps that are easier to read and write. It's like using a quick recipe card instead of a full cookbook chapter.

Testing is a critical part of modern PHP development, but traditional frameworks like PHPUnit can feel verbose and clunky. Enter Pest PHP—a testing framework designed for simplicity and elegance. Built on top of PHPUnit, Pest offers a clean, expressive syntax that makes writing tests a joy. Whether you're testing a Laravel application or a standalone PHP project, Pest reduces boilerplate and focuses on what matters: your test assertions.

In this tutorial, you'll learn how to set up Pest, write your first tests, use higher-order expectations, and integrate with Laravel. We'll cover real-world scenarios, debugging tips, and common pitfalls. By the end, you'll be able to write maintainable, readable tests that catch bugs early and speed up development.

Pest is not just a new syntax—it's a philosophy of testing that emphasizes clarity and productivity. With features like parallel testing, code coverage, and custom helpers, Pest is quickly becoming the go-to choice for PHP developers. Let's dive in and see why.

Setting Up Pest PHP

To get started with Pest, you need a PHP project with Composer. Install Pest via Composer:

``bash composer require pestphp/pest --dev ``

``bash php vendor/bin/pest --init ``

This creates a tests directory with a Pest.php configuration file. You can also install Pest for Laravel:

``bash composer require pestphp/pest-plugin-laravel --dev ``

Now you're ready to write your first test. Create a file tests/ExampleTest.php:

```php <?php

test('example', function () { expect(true)->toBeTrue(); }); ```

``bash php vendor/bin/pest ``

You should see a green output indicating the test passed. Pest automatically discovers tests in the tests directory.

tests/ExampleTest.phpPHP
1
2
3
4
5
6
<?php

test('example', function () {
    expect(true)->toBeTrue();
});
Output
PASS Tests\ExampleTest
✓ example
Tests: 1 passed (1 assertions)
💡Pest vs PHPUnit
📊 Production Insight
In production, always run pest --parallel to speed up test suites, especially in CI pipelines.
🎯 Key Takeaway
Pest uses a functional approach with test() and it() functions, making tests concise and readable.

Writing Your First Tests with Pest

Pest provides two main functions: test() and it(). Both are identical; it() reads more naturally for behavior-driven tests.

```php <?php

it('asserts true is true', function () { $this->assertTrue(true); }); ```

You can also use PHPUnit's assertion methods via $this->.... Pest also offers custom expectations like expect():

```php <?php

test('expect example', function () { expect(2 + 2)->toBe(4); expect('hello')->toContain('ell'); expect([1, 2, 3])->toHaveCount(3); }); ```

Pest's expectations are chainable and provide readable failure messages. For example, expect($value)->toBeGreaterThan(10) will output a clear message if the assertion fails.

tests/FirstTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
<?php

test('sum works', function () {
    $result = 2 + 2;
    expect($result)->toBe(4);
});

it('checks string content', function () {
    expect('hello world')->toContain('world');
});
Output
PASS Tests\FirstTest
✓ sum works
✓ checks string content
Tests: 2 passed (2 assertions)
🔥Naming Conventions
📊 Production Insight
Name your tests descriptively; they serve as documentation. Use it() for behavior-driven descriptions.
🎯 Key Takeaway
Use test() or it() for each test case. expect() provides fluent assertions with clear error messages.

Higher-Order Expectations and Custom Helpers

Pest's higher-order expectations allow you to chain multiple assertions on the same value. For example:

```php <?php

test('higher-order', function () { expect([1, 2, 3]) ->toBeArray() ->toHaveCount(3) ->each->toBeGreaterThan(0); }); ```

```php <?php

// In tests/Pest.php expect()->extend('toBeWithinRange', function (int $min, int $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); });

// In a test test('custom expectation', function () { expect(5)->toBeWithinRange(1, 10); }); ```

Custom helpers reduce duplication and make tests more expressive.

tests/HelpersTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

// tests/Pest.php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
    return $this->toBeGreaterThanOrEqual($min)
                ->toBeLessThanOrEqual($max);
});

// tests/HelpersTest.php
test('custom expectation', function () {
    expect(5)->toBeWithinRange(1, 10);
});
Output
PASS Tests\HelpersTest
✓ custom expectation
Tests: 1 passed (1 assertions)
⚠ Avoid Overusing Custom Helpers
📊 Production Insight
In large projects, create a dedicated tests/Helpers directory for custom expectations to keep Pest.php clean.
🎯 Key Takeaway
Higher-order expectations chain multiple assertions. Custom helpers encapsulate common logic.

Testing Laravel Applications with Pest

Pest integrates seamlessly with Laravel. Install the Laravel plugin:

``bash composer require pestphp/pest-plugin-laravel --dev ``

Now you can use Laravel's testing helpers like get(), post(), and assertDatabaseHas():

```php <?php

use function Pest\Laravel\get; use function Pest\Laravel\post;

test('welcome page loads', function () { get('/')->assertStatus(200); });

test('user can register', function () { post('/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password', 'password_confirmation' => 'password', ])->assertRedirect('/home');

$this->assertDatabaseHas('users', ['email' => 'john@example.com']); }); ```

You can also use Laravel's model factories and database transactions:

```php <?php

use App\Models\User; use function Pest\Laravel\actingAs;

test('authenticated user can view dashboard', function () { $user = User::factory()->create(); actingAs($user) ->get('/dashboard') ->assertOk(); }); ```

tests/LaravelTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

use function Pest\Laravel\get;
use function Pest\Laravel\post;
use function Pest\Laravel\actingAs;
use App\Models\User;

test('welcome page loads', function () {
    get('/')->assertStatus(200);
});

test('authenticated user can view dashboard', function () {
    $user = User::factory()->create();
    actingAs($user)
        ->get('/dashboard')
        ->assertOk();
});
Output
PASS Tests\LaravelTest
✓ welcome page loads
✓ authenticated user can view dashboard
Tests: 2 passed (2 assertions)
💡Database Transactions
📊 Production Insight
Always use RefreshDatabase or DatabaseTransactions to keep tests isolated and fast.
🎯 Key Takeaway
Pest's Laravel plugin provides convenient functions for HTTP tests, authentication, and database assertions.

Organizing Tests with Groups and Datasets

Pest supports test groups and datasets for better organization.

Groups: Use ->group() to tag tests:

```php <?php

test('admin test', function () { // ... })->group('admin'); ```

Run only admin tests: php vendor/bin/pest --group=admin

Datasets: Provide multiple inputs to a test:

```php <?php

test('addition', function ($a, $b, $expected) { expect($a + $b)->toBe($expected); })->with([ [1, 2, 3], [0, 0, 0], [-1, 1, 0], ]); ```

```php <?php

$data = [ 'positive' => [1, 2, 3], 'zero' => [0, 0, 0], ];

test('addition with names', function ($a, $b, $expected) { expect($a + $b)->toBe($expected); })->with($data); ```

Datasets make it easy to test multiple scenarios without duplication.

tests/DatasetTest.phpPHP
1
2
3
4
5
6
7
8
9
10
<?php

test('addition', function ($a, $b, $expected) {
    expect($a + $b)->toBe($expected);
})->with([
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
]);
Output
PASS Tests\DatasetTest
✓ addition with data set #1
✓ addition with data set #2
✓ addition with data set #3
Tests: 3 passed (3 assertions)
🔥Dataset File
📊 Production Insight
Use groups to separate unit, feature, and integration tests. Run only relevant groups in CI to save time.
🎯 Key Takeaway
Groups and datasets help organize tests and reduce repetition.

Mocking and Test Doubles with Pest

Pest works with PHPUnit's mocking framework. You can create mocks using $this->createMock() or use Mockery:

```php <?php

use App\Services\PaymentService;

test('payment processing', function () { $paymentMock = $this->createMock(PaymentService::class); $paymentMock->method('charge')->willReturn(true);

$result = $paymentMock->charge(100); expect($result)->toBeTrue(); }); ```

``bash composer require mockery/mockery --dev ``

```php <?php

test('mockery example', function () { $mock = Mockery::mock(PaymentService::class); $mock->shouldReceive('charge')->once()->with(100)->andReturn(true);

$result = $mock->charge(100); expect($result)->toBeTrue(); }); ```

```php <?php

use Mockery;

beforeEach(function () { // setup });

afterEach(function () { Mockery::close(); }); ```

tests/MockingTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
<?php

use App\Services\PaymentService;

test('payment processing with mock', function () {
    $paymentMock = $this->createMock(PaymentService::class);
    $paymentMock->method('charge')->willReturn(true);

    $result = $paymentMock->charge(100);
    expect($result)->toBeTrue();
});
Output
PASS Tests\MockingTest
✓ payment processing with mock
Tests: 1 passed (1 assertions)
⚠ Mockery Cleanup
📊 Production Insight
Mock external services (APIs, payment gateways) to avoid network calls in tests. Use contract testing for critical integrations.
🎯 Key Takeaway
Pest supports PHPUnit mocks and Mockery for isolating dependencies.

Code Coverage and Parallel Testing

``bash php vendor/bin/pest --coverage ``

``bash php vendor/bin/pest --coverage-html=coverage ``

``bash php vendor/bin/pest --parallel ``

``xml <phpunit> <testsuite name="default"> <directory>tests</directory> </testsuite> <coverage> <include> <directory>app</directory> </include> </coverage> </phpunit> ``

Parallel testing is especially useful in CI pipelines.

phpunit.xmlXML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         parallel="4">
    <testsuite name="default">
        <directory>tests</directory>
    </testsuite>
    <coverage>
        <include>
            <directory>app</directory>
        </include>
    </coverage>
</phpunit>
💡Parallel Testing Requirements
📊 Production Insight
Set a minimum coverage threshold in CI (e.g., 80%) to enforce quality. Use parallel testing to reduce CI time.
🎯 Key Takeaway
Use --coverage to measure test coverage and --parallel to speed up test suites.
● Production incidentPOST-MORTEMseverity: high

The Silent Test Failure: When Pest Tests Pass Locally but Fail in CI

Symptom
All tests passed on local machines but failed on the CI server with mysterious errors.
Assumption
The developer assumed the CI environment was misconfigured or missing dependencies.
Root cause
A test relied on a specific PHP extension (e.g., ext-mysqli) that was enabled locally but not in the CI Docker image. Pest's test runner didn't provide a clear error message about the missing extension.
Fix
Added a beforeEach hook to check for required extensions and skip tests gracefully. Also updated the CI Dockerfile to include the missing extension.
Key lesson
  • Always test in an environment that mirrors production as closely as possible.
  • Use Pest's skip function to handle missing dependencies gracefully.
  • Add environment checks in your test suite to fail early with clear messages.
  • Document required PHP extensions for your project.
Production debug guideSymptom to Action4 entries
Symptom · 01
Test fails only in CI, not locally
Fix
Check environment differences: PHP version, extensions, config files. Use pest --debug to see detailed output.
Symptom · 02
Test passes but code coverage is low
Fix
Run pest --coverage to identify untested lines. Add tests for edge cases.
Symptom · 03
Test is slow or times out
Fix
Use pest --parallel to run tests concurrently. Check for database transactions or external API calls.
Symptom · 04
Higher-order expectation fails unexpectedly
Fix
Break down the chain into separate assertions to isolate the issue. Use dd() inside a test to inspect values.
★ Quick Debug Cheat SheetCommon Pest issues and immediate fixes
Test not found
Immediate action
Check file naming: must end with `Test.php` and be in `tests/` directory.
Commands
php vendor/bin/pest --filter='test name'
ls tests/
Fix now
Rename file to *Test.php or run pest --help for options.
Class not found+
Immediate action
Run `composer dump-autoload`.
Commands
composer dump-autoload
php vendor/bin/pest --debug
Fix now
Ensure namespace matches directory structure.
Database test fails+
Immediate action
Check if database is migrated and seeded.
Commands
php artisan migrate:fresh --seed
php vendor/bin/pest --group=db
Fix now
Use RefreshDatabase trait in your test file.
FeaturePest PHPPHPUnit
SyntaxFunctional (test(), it())Class-based (methods)
Assertionsexpect() fluent APIassert* methods
Custom Helpersexpect()->extend()Custom assertions via traits
Parallel TestingBuilt-in (--parallel)Requires extension or config
Laravel IntegrationOfficial pluginBuilt-in via TestCase
Learning CurveLowModerate
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
testsExampleTest.phptest('example', function () {Setting Up Pest PHP
testsFirstTest.phptest('sum works', function () {Writing Your First Tests with Pest
testsHelpersTest.phpexpect()->extend('toBeWithinRange', function (int $min, int $max) {Higher-Order Expectations and Custom Helpers
testsLaravelTest.phpuse function Pest\Laravel\get;Testing Laravel Applications with Pest
testsDatasetTest.phptest('addition', function ($a, $b, $expected) {Organizing Tests with Groups and Datasets
testsMockingTest.phpuse App\Services\PaymentService;Mocking and Test Doubles with Pest
phpunit.xmlCode Coverage and Parallel Testing

Key takeaways

1
Pest PHP offers a clean, expressive syntax for testing, built on PHPUnit.
2
Use test() and it() for test cases, and expect() for fluent assertions.
3
Laravel integration provides convenient HTTP and database testing helpers.
4
Organize tests with groups and datasets to reduce duplication.
5
Leverage parallel testing and code coverage for efficient CI pipelines.

Common mistakes to avoid

3 patterns
×

Forgetting to call `Mockery::close()` after tests

×

Using `assertTrue()` instead of `expect()->toBeTrue()`

×

Not using `RefreshDatabase` in Laravel tests

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What are the main differences between Pest and PHPUnit?
Q02SENIOR
How do you create a custom expectation in Pest?
Q03SENIOR
Explain how to test a Laravel controller that sends an email using Pest.
Q01 of 03JUNIOR

What are the main differences between Pest and PHPUnit?

ANSWER
Pest uses a functional syntax with test() and it() functions instead of class methods. It provides higher-order expectations, custom helpers, and a more concise API. Under the hood, Pest runs on PHPUnit.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Pest PHP?
02
Can I use Pest with Laravel?
03
How do I run a single test with Pest?
04
Does Pest support data providers?
05
Can I use PHPUnit assertions in Pest?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
PHPStan: Static Analysis for PHP
24 / 29 · Advanced PHP
Next
PHP Deployment with Docker and CI/CD