# Pest PHP Testing Framework Pest is an elegant PHP testing framework built on top of PHPUnit, designed to bring simplicity and expressiveness to PHP testing. It provides a fluent, BDD-style API with functions like `test()`, `it()`, and `describe()` to define tests, along with a powerful `expect()` API for writing readable assertions. Pest supports datasets for parameterized testing, test hooks (`beforeEach`, `afterEach`, `beforeAll`, `afterAll`), and integrates seamlessly with PHPUnit's existing ecosystem. The framework emphasizes developer experience with features like architecture testing (ensuring code follows specific patterns), mutation testing, parallel test execution, and browser testing capabilities. Pest v4 introduces enhanced configuration through the `pest()` function, improved type coverage analysis, and first-class support for Laravel applications through dedicated presets. It requires PHP 8.3+ and leverages modern PHP features like attributes, closures, and generator functions. ## Core Test Functions ### test() - Define a Test Case The `test()` function is the primary way to define test cases in Pest. It accepts a description string and a closure containing your test logic. Tests can be chained with modifiers like `skip()`, `only()`, `todo()`, and `throws()`. ```php toBe(3); }); // Test with skip condition test('requires redis connection', function () { // Test logic that requires Redis expect(true)->toBeTrue(); })->skip(fn () => !extension_loaded('redis'), 'Redis extension not available'); // Test that expects an exception test('throws exception on invalid input', function () { throw new InvalidArgumentException('Invalid input'); })->throws(InvalidArgumentException::class, 'Invalid input'); // Mark test as todo test('implement user registration')->todo(); // Test with dependencies test('first test', function () { return 'value'; }); test('second test', function () { expect(true)->toBeTrue(); })->depends('first test'); ``` ### it() - BDD-Style Test Definition The `it()` function is an alias for `test()` that automatically prefixes descriptions with "it", enabling BDD-style test naming. ```php toBe(5); }); it('handles negative numbers correctly', function () { expect(add(-1, 1))->toBe(0); }); // Chained with modifiers it('processes large datasets') ->skip(fn () => memory_get_usage() > 100000000, 'Low memory') ->throwsNoExceptions(); ``` ### describe() - Group Related Tests The `describe()` function groups related tests together, enabling nested test organization with shared hooks. Describe blocks can be nested and each can have their own `beforeEach` and `afterEach` hooks. ```php user = new User(['email' => 'test@example.com']); }); test('can login with valid credentials', function () { expect($this->user->authenticate('password123'))->toBeTrue(); }); test('fails with invalid password', function () { expect($this->user->authenticate('wrong'))->toBeFalse(); }); describe('Two-Factor Authentication', function () { beforeEach(function () { $this->user->enable2FA(); }); test('requires 2FA code', function () { expect($this->user->requires2FA())->toBeTrue(); }); test('validates 2FA code', function () { expect($this->user->verify2FA('123456'))->toBeTrue(); }); }); }); // Describe with datasets describe('Math Operations', function () { test('adds numbers correctly', function ($a, $b, $expected) { expect($a + $b)->toBe($expected); }); })->with([ [1, 2, 3], [0, 0, 0], [-1, 1, 0], ]); ``` ## Test Hooks ### beforeEach() and afterEach() - Per-Test Hooks These hooks run before and after each individual test in the current file or describe block. Use them to set up and tear down test fixtures. ```php database = new Database(); $this->database->beginTransaction(); }); afterEach(function () { $this->database->rollback(); }); test('can create user', function () { $user = $this->database->createUser(['name' => 'John']); expect($user)->not->toBeNull(); }); test('can delete user', function () { $user = $this->database->createUser(['name' => 'Jane']); $this->database->deleteUser($user->id); expect($this->database->findUser($user->id))->toBeNull(); }); ``` ### beforeAll() and afterAll() - Per-File Hooks These hooks run once before all tests in a file start and after all tests complete. Useful for expensive setup operations. ```php id(); $table->string('name'); }); }); afterAll(function () { // Clean up after all tests Schema::dropIfExists('users'); }); test('database schema exists', function () { expect(Schema::hasTable('users'))->toBeTrue(); }); ``` ## Expectations API ### expect() - Fluent Assertions The `expect()` function creates an expectation chain for making assertions. It provides dozens of assertion methods that can be chained together. ```php toBe(5); expect('hello')->toEqual('hello'); // Type checks expect(42)->toBeInt(); expect(3.14)->toBeFloat(); expect('test')->toBeString(); expect([1, 2, 3])->toBeArray(); expect(new stdClass)->toBeObject(); // Boolean assertions expect(true)->toBeTrue(); expect(false)->toBeFalse(); expect(1)->toBeTruthy(); expect(0)->toBeFalsy(); // Null checks expect(null)->toBeNull(); expect('value')->not->toBeNull(); // Comparison expect(10)->toBeGreaterThan(5); expect(5)->toBeLessThan(10); expect(5)->toBeBetween(1, 10); // String assertions expect('hello world')->toContain('world'); expect('hello world')->toStartWith('hello'); expect('hello world')->toEndWith('world'); expect('test@example.com')->toMatch('/^[\w\.-]+@[\w\.-]+\.\w+$/'); // Array assertions expect(['a', 'b', 'c'])->toContain('b'); expect(['name' => 'John', 'age' => 30])->toHaveKey('name'); expect(['a', 'b', 'c'])->toHaveCount(3); expect(['name' => 'John'])->toMatchArray(['name' => 'John']); // Object assertions $user = new User(['name' => 'John']); expect($user)->toBeInstanceOf(User::class); expect($user)->toHaveProperty('name', 'John'); // Exception assertion expect(fn () => throw new Exception('error'))->toThrow(Exception::class); }); ``` ### not - Negated Expectations The `not` property inverts any expectation that follows it. ```php not->toBeEmpty(); expect(5)->not->toBe(10); expect(['a', 'b'])->not->toContain('c'); expect(new User)->not->toBeInstanceOf(Admin::class); expect('test')->not->toMatch('/\d+/'); }); ``` ### each - Iterate Over Collections The `each` property creates expectations that apply to every item in an iterable. ```php 'John', 'active' => true], ['name' => 'Jane', 'active' => true], ['name' => 'Bob', 'active' => true], ]; expect($users)->each->toHaveKey('name'); expect($users)->each->toHaveKey('active', true); // With callback expect([1, 2, 3])->each(function ($value) { $value->toBeInt()->toBeGreaterThan(0); }); }); ``` ### sequence() - Sequential Expectations The `sequence()` method applies different expectations to each element in order. ```php sequence( fn ($item) => $item->toBe('first'), fn ($item) => $item->toBe('second'), fn ($item) => $item->toBe('third'), ); // Or with values directly expect([1, 2, 3])->sequence(1, 2, 3); }); ``` ### extend() - Custom Expectations Extend the `expect()` function with custom assertion methods. ```php extend('toBeValidEmail', function () { return $this->toMatch('/^[\w\.-]+@[\w\.-]+\.\w+$/'); }); expect()->extend('toBePositive', function () { return $this->toBeGreaterThan(0); }); // Usage test('custom expectations', function () { expect('user@example.com')->toBeValidEmail(); expect(42)->toBePositive(); }); ``` ## Datasets - Parameterized Testing ### with() - Inline Datasets The `with()` method provides test data for parameterized testing. Tests run once for each dataset entry. ```php toBe($expected); })->with([ ['valid@example.com', true], ['invalid-email', false], ['another@test.org', true], ['missing-at-sign.com', false], ]); // Named datasets test('arithmetic operations', function (int $a, int $b, int $expected) { expect($a + $b)->toBe($expected); })->with([ 'positive numbers' => [1, 2, 3], 'negative numbers' => [-1, -2, -3], 'mixed numbers' => [-1, 2, 1], 'zeros' => [0, 0, 0], ]); // Multiple datasets (creates cartesian product) test('string concatenation', function (string $prefix, string $suffix) { expect($prefix . $suffix)->toBeString(); })->with(['Hello', 'Hi'])->with([' World', ' There']); // Runs: Hello World, Hello There, Hi World, Hi There ``` ### dataset() - Shared Datasets Define reusable datasets that can be referenced by name across multiple tests. ```php ['user@gmail.com'], 'company' => ['employee@company.org'], 'subdomain' => ['admin@mail.example.com'], ]); dataset('users', function () { yield 'admin' => [new User(['role' => 'admin'])]; yield 'editor' => [new User(['role' => 'editor'])]; yield 'viewer' => [new User(['role' => 'viewer'])]; }); // Usage in tests test('sends welcome email', function (string $email) { expect(sendEmail($email))->toBeTrue(); })->with('valid-emails'); test('user has permissions', function (User $user) { expect($user->hasPermissions())->toBeTrue(); })->with('users'); ``` ## Configuration ### pest() - Global Configuration The `pest()` function configures Pest behavior for your test suite. Place it in `tests/Pest.php`. ```php extends(TestCase::class); // Apply traits globally pest()->use(RefreshDatabase::class, WithFaker::class); // Target specific directories pest()->extends(TestCase::class)->in('Feature'); pest()->extends(UnitTestCase::class)->in('Unit'); // Add groups pest()->group('api')->in('Feature/Api'); // Configure global hooks pest()->beforeEach(function () { $this->app = createApplication(); }); pest()->afterEach(function () { $this->app->terminate(); }); ``` ### uses() - Per-File Configuration The `uses()` function applies traits or base classes to tests in a specific file. ```php create(); $this->assertDatabaseHas('users', ['id' => $user->id]); }); // With beforeEach hook uses(TestCase::class) ->beforeEach(fn () => $this->seed(UserSeeder::class)); ``` ## Architecture Testing ### Arch Expectations - Code Structure Validation Pest provides architecture testing to ensure your codebase follows specific patterns and conventions. ```php expect('App\Http\Controllers') ->toHaveSuffix('Controller'); arch('models extend eloquent') ->expect('App\Models') ->toExtend('Illuminate\Database\Eloquent\Model'); arch('no debugging statements') ->expect(['dd', 'dump', 'var_dump', 'print_r']) ->not->toBeUsed(); arch('interfaces are in contracts directory') ->expect('App\Contracts') ->toBeInterfaces(); arch('services use dependency injection') ->expect('App\Services') ->not->toUse(['request', 'session']); arch('strict types everywhere') ->expect('App') ->toUseStrictTypes(); arch('domain isolation') ->expect('App\Domain\Orders') ->toOnlyBeUsedIn(['App\Domain\Orders', 'App\Http\Controllers\OrderController']); // Class structure arch('value objects are final and readonly') ->expect('App\ValueObjects') ->toBeFinal() ->toBeReadonly(); arch('enums are string-backed') ->expect('App\Enums') ->toBeStringBackedEnums(); arch('exceptions implement throwable') ->expect('App\Exceptions') ->toImplement(Throwable::class); ``` ### Architecture Presets Use built-in presets for common architecture rules. ```php preset()->laravel(); // Strict preset - enforces strict coding standards arch()->preset()->strict(); // Security preset - checks for security issues arch()->preset()->security(); // PHP preset - general PHP best practices arch()->preset()->php(); // Relaxed preset - basic standards arch()->preset()->relaxed(); // Custom presets pest()->presets()->custom(function () { return [ expect('App')->toUseStrictTypes(), expect('App')->classes()->toBeFinal(), ]; }); ``` ## Test Modifiers ### skip() - Conditionally Skip Tests Skip tests based on conditions or provide a reason for skipping. ```php skip('Database not configured'); test('windows only feature') ->skip(PHP_OS_FAMILY !== 'Windows', 'Only runs on Windows'); test('needs redis', function () { // test code })->skip(fn () => !extension_loaded('redis')); // Built-in skip helpers test('ci specific')->skipOnCI(); test('local only')->skipLocally(); test('not on windows')->skipOnWindows(); test('not on mac')->skipOnMac(); test('not on linux')->skipOnLinux(); test('php 8.2+ only')->skipOnPhp('<8.2'); ``` ### throws() - Exception Assertions Declare that a test should throw specific exceptions. ```php throws(InvalidArgumentException::class); test('throws with message', function () { throw new RuntimeException('Connection failed'); })->throws(RuntimeException::class, 'Connection failed'); test('throws with code', function () { throw new Exception('Error', 500); })->throws(Exception::class, exceptionCode: 500); // Conditional throws test('throws in production', function () { throw new Exception('Not allowed'); })->throwsIf(app()->environment('production'), Exception::class); // Assert test doesn't throw test('completes without exception', function () { riskyOperation(); })->throwsNoExceptions(); ``` ### group() and covers() - Test Organization Organize tests into groups and specify code coverage. ```php group('api', 'integration'); test('user service', function () { // test })->covers(UserService::class); test('helper function', function () { // test })->coversFunction('format_date'); // Run specific groups // ./vendor/bin/pest --group=api // ./vendor/bin/pest --exclude-group=slow ``` ### repeat() - Run Tests Multiple Times Repeat a test multiple times to check for flaky behavior. ```php status())->toBe(200); })->repeat(100); ``` ## Higher-Order Tests ### Fluent Test Definitions Write tests using a fluent, chainable syntax without explicit closures. ```php expect(fn () => new User(['name' => 'John', 'active' => true])) ->toHaveProperty('name', 'John') ->toHaveProperty('active', true); // With datasets test('validates input') ->with(['valid@email.com', 'another@test.org']) ->expect(fn (string $email) => filter_var($email, FILTER_VALIDATE_EMAIL)) ->not->toBeFalse(); // Higher-order hooks beforeEach() ->skip(fn () => !database_configured()) ->expect(fn () => DB::connection()->getPdo()) ->not->toBeNull(); ``` ## Summary Pest PHP revolutionizes testing in PHP by combining the power of PHPUnit with an elegant, expressive syntax. Its core strength lies in the natural language-like test definitions using `test()`, `it()`, and `describe()`, paired with the fluent `expect()` API that makes assertions readable and maintainable. The dataset system enables powerful parameterized testing, while architecture testing ensures code quality at a structural level. For integration into existing projects, Pest works seamlessly with Laravel and other PHP frameworks. Configure global settings in `tests/Pest.php`, create shared datasets in `tests/Datasets/`, and organize tests using describe blocks and groups. The framework's plugin architecture supports extensions for browser testing, mutation testing, type coverage, and more. Whether migrating from PHPUnit or starting fresh, Pest's backward compatibility and progressive enhancement model makes adoption straightforward while delivering immediate improvements in test readability and developer experience.