Pest PHP: Modern Testing Framework for Laravel & PHP
Learn Pest PHP testing framework with practical examples, debugging tips, and real-world scenarios.
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
- ✓Basic knowledge of PHP and Composer
- ✓Familiarity with PHPUnit (optional but helpful)
- ✓A PHP project (Laravel or plain PHP)
- Pest PHP is a modern testing framework built on top of PHPUnit with a simpler syntax.
- It uses functions like
test()andit()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.
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 ``
Then initialize Pest in your project:
``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(); }); ```
Run tests with:
``bash php vendor/bin/pest ``
You should see a green output indicating the test passed. Pest automatically discovers tests in the tests directory.
pest --parallel to speed up test suites, especially in CI pipelines.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.
Example:
```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.
it() for behavior-driven descriptions.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); }); ```
You can also create custom helpers using :expect()->extend()
```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/Helpers directory for custom expectations to keep Pest.php clean.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(), and post()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(); }); ```
RefreshDatabase or DatabaseTransactions to keep tests isolated and fast.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], ]); ```
You can also use named datasets:
```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.
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(); }); ```
For more expressive mocking, use Mockery:
``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(); }); ```
Remember to clean up Mockery after tests:
```php <?php
use Mockery;
beforeEach(function () { // setup });
afterEach(function () { Mockery::close(); }); ```
Code Coverage and Parallel Testing
Pest provides built-in code coverage via Xdebug or PCOV. Run:
``bash php vendor/bin/pest --coverage ``
To generate HTML reports:
``bash php vendor/bin/pest --coverage-html=coverage ``
Parallel testing speeds up execution:
``bash php vendor/bin/pest --parallel ``
You can configure processes in phpunit.xml:
``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.
--coverage to measure test coverage and --parallel to speed up test suites.The Silent Test Failure: When Pest Tests Pass Locally but Fail in CI
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.beforeEach hook to check for required extensions and skip tests gracefully. Also updated the CI Dockerfile to include the missing extension.- Always test in an environment that mirrors production as closely as possible.
- Use Pest's
skipfunction 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.
pest --debug to see detailed output.pest --coverage to identify untested lines. Add tests for edge cases.pest --parallel to run tests concurrently. Check for database transactions or external API calls.dd() inside a test to inspect values.php vendor/bin/pest --filter='test name'ls tests/*Test.php or run pest --help for options.| File | Command / Code | Purpose |
|---|---|---|
| tests | test('example', function () { | Setting Up Pest PHP |
| tests | test('sum works', function () { | Writing Your First Tests with Pest |
| tests | expect()->extend('toBeWithinRange', function (int $min, int $max) { | Higher-Order Expectations and Custom Helpers |
| tests | use function Pest\Laravel\get; | Testing Laravel Applications with Pest |
| tests | test('addition', function ($a, $b, $expected) { | Organizing Tests with Groups and Datasets |
| tests | use App\Services\PaymentService; | Mocking and Test Doubles with Pest |
| phpunit.xml | | Code Coverage and Parallel Testing | |
Key takeaways
test() and it() for test cases, and expect() for fluent assertions.Common mistakes to avoid
3 patternsForgetting to call `Mockery::close()` after tests
Using `assertTrue()` instead of `expect()->toBeTrue()`
Not using `RefreshDatabase` in Laravel tests
Interview Questions on This Topic
What are the main differences between Pest and PHPUnit?
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.Frequently Asked Questions
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
That's Advanced PHP. Mark it forged?
3 min read · try the examples if you haven't