Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Pest
https://github.com/pestphp/pest
Admin
Pest is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring
...
Tokens:
4,565
Snippets:
39
Trust Score:
8.4
Update:
4 weeks ago
Context
Skills
Chat
Benchmark
64.8
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# 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 <?php // Basic test test('it can add two numbers', function () { $result = 1 + 2; expect($result)->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 <?php it('returns the correct sum', function () { expect(add(2, 3))->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 <?php describe('User Authentication', function () { beforeEach(function () { $this->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 <?php beforeEach(function () { $this->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 <?php beforeAll(function () { // Create test database schema once Schema::create('users', function ($table) { $table->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 <?php // Basic assertions test('expectation examples', function () { // Equality expect(5)->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 <?php test('negated expectations', function () { expect('hello')->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 <?php test('each element matches expectation', function () { $users = [ ['name' => '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 <?php test('sequence expectations', function () { $items = ['first', 'second', 'third']; expect($items)->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 <?php // In tests/Pest.php or a test file expect()->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 <?php test('validates email format', function (string $email, bool $expected) { expect(filter_var($email, FILTER_VALIDATE_EMAIL) !== false)->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 <?php // In tests/Datasets/Users.php or any test file dataset('valid-emails', [ 'gmail' => ['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 <?php // tests/Pest.php use Tests\TestCase; // Extend base test case for all tests pest()->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 <?php // In a specific test file uses(Tests\TestCase::class, RefreshDatabase::class); test('creates user in database', function () { $user = User::factory()->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 <?php // tests/Arch.php arch('controllers have correct suffix') ->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 <?php // tests/Pest.php // Laravel preset - enforces Laravel conventions arch()->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 <?php test('requires database')->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 <?php test('throws on invalid argument', function () { validate(null); })->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 <?php test('api endpoint', function () { // test })->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 <?php test('handles concurrent requests', function () { $response = makeRequest(); expect($response->status())->toBe(200); })->repeat(100); ``` ## Higher-Order Tests ### Fluent Test Definitions Write tests using a fluent, chainable syntax without explicit closures. ```php <?php // Higher-order expectations test('user properties') ->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.