Introduction to Pest

+15 Mana ✨

Introduction

Pest is a testing framework built on top of PHPUnit that replaces the class-based, method-per-test style with top-level test() functions and a fluent expect() API. It ships pre-installed with every new Laravel app since Laravel 11, and the Laravel docs now use it as the default syntax in examples.

Key Concepts

  • test('description', closure): Declares a test. The closure is the test body.
  • it('does something', closure): Alias for test() that reads more like a sentence (BDD style).
  • expect($value)->toBe(...): Fluent assertion API. Each matcher returns the expectation so you can chain.

Real World Context

Every test framework has friction. PHPUnit's is the ceremony — a namespace, a class, a method with test_ prefix, public function, : void, $this->. Pest strips all of that: a file with test(...) calls at the top level runs instantly. The reduced friction means people actually write the test instead of putting it off.

Deep Dive

Your first Pest test

php
// tests/Feature/HomeTest.php
<?php

test('the home page returns a 200', function () {
    $response = $this->get('/');

    expect($response->status())->toBe(200);
});

it('shows the app name', function () {
    $this->get('/')
        ->assertSee(config('app.name'));
});

No namespace, no class, no public function. Run it with php artisan test — Pest tests run alongside PHPUnit tests in the same suite.

The expect() API

php
expect($value)->toBe(1);                    // strict equality
expect($value)->toEqual(1);                  // loose equality
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($value)->toBeNull();
expect($value)->toBeInstanceOf(User::class);
expect($array)->toHaveCount(3);
expect($array)->toContain('laravel');
expect($string)->toStartWith('Hello');
expect($string)->toMatch('/^[a-z]+$/');
expect($collection)->each->toBeInstanceOf(Post::class);  // Higher-order

The last form is Pest's killer feature: each->toBeInstanceOf(...) applies the assertion to every element in the collection, no foreach required.

Keeping Laravel helpers

Inside a Pest test closure, $this is bound to a TestCase instance, so you get every Laravel helper:

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

    $response = $this->actingAs($user)
        ->post('/posts', ['title' => 'Hello']);

    $response->assertRedirect('/posts');
    $this->assertDatabaseHas('posts', ['title' => 'Hello']);
});

Converting a PHPUnit test

Before (PHPUnit):

php
class UserTest extends TestCase
{
    public function test_full_name(): void
    {
        $user = new User(['first_name' => 'Ada', 'last_name' => 'Lovelace']);

        $this->assertEquals('Ada Lovelace', $user->full_name);
    }
}

After (Pest):

php
test('full name combines first and last', function () {
    $user = new User(['first_name' => 'Ada', 'last_name' => 'Lovelace']);

    expect($user->full_name)->toBe('Ada Lovelace');
});

Common Pitfalls

  1. Declaring namespace at the top of a Pest file — don't. Pest files are top-level scripts and a namespace breaks the autoloader discovery.
  2. Using $this->expects(...) thinking it's Mockery — Mockery's method is shouldReceive. Pest's expect() is unrelated and doesn't live on $this.

Best Practices

  1. Prefer it() for behavioral tests — reading it creates a post as a sentence makes the intent obvious.
  2. Use higher-order each — expect($posts)->each->toBeInstanceOf(Post::class) is tighter than a loop.

Summary

  • Pest replaces class-per-file with top-level test() / it() functions.
  • expect() is a fluent, chainable assertion API.
  • Inside the closure, $this is a Laravel TestCase — every helper still works.
  • PHPUnit and Pest tests can coexist in the same suite.
✓ Completed