1. Project Overview & Quickstart (giggsey/METADATA-VERSION.php)
METADATA-VERSION.php
Open-source repository giggsey/METADATA-VERSION.php
Repository Details
- Repository: giggsey/METADATA-VERSION.php
- Primary Language: Code
Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.
2. Official Technical Reference & Guides (giggsey/docs)
File: README.md
This repository contains the Pest Documentation.
If you want to start testing your application with Pest, visit the main Pest Repository.
- Explore our docs at pestphp.com »
- Follow us on Twitter at @pestphp »
- Join us at discord.gg/kaHY6p54JH » or t.me/+kYH5G4d5MV83ODk0 »
Pest is an open-sourced software licensed under the MIT license.
File: announcing-pest2.md
title: Announcing Pest 2.0
Announcing Pest 2.0
The Pest team is thrilled to unveil the release of Pest 2.0 after a development period of 18 months and over 500 commits. This release introduces several exciting features that promise to improve the user's experience. Among the notable enhancements are robust new plugins, refined syntax, and advanced options that streamline testing, enhance usability, and boost productivity.
Today we’re finally making the long-awaited release of Pest 2.0! Our creator is eager to showcase the exciting new features this version has to offer. Tune in to the video below to learn more.
Pest 2.0 marks a major milestone in our development, packed with an array of powerful features such as:
- Powerful Architecture Plugin, for testing the architectural rules of your application with ease
- Up To 80% Speed Improvements on "--parallel" testing, with our fully rewritten parallel core, enjoy significantly faster test runs
- --profile option, to identify the slowest tests and optimize their execution
- --compact printer, a minimal printer that only outputs information about test failures
- --retry option, for saving time by running only previously unsuccessful tests
- --dirty option, for only running tests with uncommitted changes
- --bail option, to immediately terminate the test suite upon encountering an error or failure.
- todo() method, for creating todos within your test suite
- Expectation Interceptors and Pipes, allowing you to tailor your expectations to fit your specific testing needs
- Scoped Datasets, for creating datasets that pertain only to a specific feature or set of folders
In addition to the features detailed above, there's so much more to explore with Pest 2.0! Our website has been completely revamped, with fresh documentation and a more user-friendly interface. There's never been a better time to dive in and start exploring.
If you're ready to get started with Pest 2.0 right away, check out our installation guide for step-by-step instructions. And if you're currently using Pest 1, we've got you covered with detailed upgrade instructions in our upgrade guide.
Thank you for reading about Pest 2.0's new features! If you're considering a testing framework for your next project, here's why you should give Pest a try: Why Pest →
File: announcing-stressless.md
title: Announcing Stressless
Announcing Stressless
We are thrilled to announce the release of a brand new plugin for Pest PHP: Stressless.
It's a fresh new addition to the Pest PHP family, and it brings the power of stress testing to the PHP ecosystem. It integrates seamlessly with Pest PHP, combining the power of stress testing with the simplicity and elegance of Pest's Expectation API.
Check out this YouTube video where we walk you through the installation and setup of the Stressless plugin:
As you can see, it's effortless to get started with Stressless — all you need to do is require the package using Composer, and you're ready to go!
There are two main ways to use Stressless. You may use it to quickly stress test your application from the command line:
./vendor/bin/pest stress example.com --concurrency=5 --duration=10Or you can use it to write stress tests in your Pest PHP test files:
<?php
test('black friday', function () {
$result = stress('example.com')
->concurrently(5)
->for(10)->seconds();
$requests = $result->requests;
expect($requests->failed->count)
->toBe(0);
expect($requests->duration->med)
->toBeLessThan(100.0); // 100ms
});Check our documentation to get started with Stress Testing / Stressless: Stress Testing →. We hope you enjoy this new addition to the Pest PHP family!
If you're considering a testing framework for your next project, here's why you should give Pest a try: Why Pest →
File: arch-testing.md
title: Architecture Testing
Architecture Testing
Architecture testing enables you to specify expectations that test whether your application adheres to a set of architectural rules, helping you maintain a clean and sustainable codebase. The expectations are determined by either relative namespaces, fully qualified namespaces, or function names.
Here is an example of how you can define an architectural rule:
arch()
->expect('App')
->toUseStrictTypes()
->not->toUse(['die', 'dd', 'dump']);
arch()
->expect('App\Models')
->toBeClasses()
->toExtend('Illuminate\Database\Eloquent\Model')
->toOnlyBeUsedIn('App\Repositories')
->ignoring('App\Models\User');
arch()
->expect('App\Http')
->toOnlyBeUsedIn('App\Http');
arch()
->expect('App\*\Traits')
->toBeTraits();
arch()->preset()->php();
arch()->preset()->security()->ignoring('md5');Now, let's dive into the various methods and modifiers available for architectural testing. In this section, you will learn:
- Expectations: Allows to specify granular architectural rules.
- Presets: Allows to use predefined sets of granular architectural rules.
- Modifiers: To exclude or ignore certain types of files, classes, functions or lines of code.
Expectations
Granular expectations allow you to define specific architectural rules for your application. Here are the available expectations:
toBeAbstract()toBeClasses()toBeEnums()toBeIntBackedEnums()toBeInterfaces()toBeInvokable()toBeFinal()toBeReadonly()toBeStringBackedEnums()toBeTraits()toBeUsed()toBeUsedIn()toExtend()toExtendNothing()toImplement()toImplementNothing()toHaveMethodsDocumented()toHavePropertiesDocumented()toHaveAttribute()toHaveFileSystemPermissions()toHaveLineCountLessThan()toHaveMethod()toHaveMethods()toHavePrivateMethodsBesides()toHavePrivateMethods()toHaveProtectedMethodsBesides()toHaveProtectedMethods()toHavePublicMethodsBesides()toHavePublicMethods()toHavePrefix()toHaveSuffix()toHaveConstructor()toHaveDestructor()toOnlyImplement()toOnlyUse()toOnlyBeUsedIn()toUse()toUseStrictEquality()toUseTrait()toUseTraits()toUseNothing()toUseStrictTypes()
`toBeAbstract()`
The toBeAbstract() method may be used to ensure that all classes within a given namespace are abstract.
arch('app')
->expect('App\Models')
->toBeAbstract();`toBeClasses()`
The toBeClasses() method may be used to ensure that all files within a given namespace are classes.
arch('app')
->expect('App\Models')
->toBeClasses();`toBeEnums()`
The toBeEnums() method may be used to ensure that all files within a given namespace are enums.
arch('app')
->expect('App\Enums')
->toBeEnums();`toBeIntBackedEnums()`
The toBeIntBackedEnums() method may be used to ensure that all enums within a specified namespace are int-backed.
arch('app')
->expect('App\Enums')
->toBeIntBackedEnums();`toBeInterfaces()`
The toBeInterfaces() method may be used to ensure that all files within a given namespace are interfaces.
arch('app')
->expect('App\Contracts')
->toBeInterfaces();`toBeInvokable()`
The toBeInvokable() method may be used to ensure that all files within a given namespace are invokable.
arch('app')
->expect('App\Actions')
->toBeInvokable();`toBeTraits()`
The toBeTraits() method may be used to ensure that all files within a given namespace are traits.
arch('app')
->expect('App\Concerns')
->toBeTraits();`toBeFinal()`
The toBeFinal() method may be used to ensure that all classes within a given namespace are final.
arch('app')
->expect('App\ValueObjects')
->toBeFinal();Note that, typically this expectation is used in combination with the classes() modifier to ensure that all classes within a given namespace are final.
arch('app')
->expect('App')
->classes()
->toBeFinal();`toBeReadonly()`
The toBeReadonly() method may be used to ensure that certain classes are immutable and cannot be modified at runtime.
arch('app')
->expect('App\ValueObjects')
->toBeReadonly();Note that, typically this expectation is used in combination with the classes() modifier to ensure that all classes within a given namespace are readonly.
arch('app')
->expect('App')
->classes()
->toBeReadonly();`toBeStringBackedEnums()`
The toBeStringBackedEnums() method may be used to ensure that all enums within a specified namespace are string-backed.
arch('app')
->expect('App\Enums')
->toBeStringBackedEnums();`toBeUsed()`
The not modifier, when combined with the toBeUsed() method, enables you to verify that certain classes or functions are not being utilized by your application.
arch('globals')
->expect(['dd', 'dump'])
->not->toBeUsed();
arch('facades')
->expect('Illuminate\Support\Facades')
->not->toBeUsed();`toBeUsedIn()`
By combining the not modifier with the toBeUsedIn() method, you can restrict specific classes and functions from being used within a given namespace.
arch('globals')
->expect('request')
->not->toBeUsedIn('App\Domain');
arch('globals')
->expect('Illuminate\Http')
->not->toBeUsedIn('App\Domain');`toExtend()`
The toExtend() method may be used to ensure that all classes within a given namespace extend a specific class.
arch('app')
->expect('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model');`toExtendNothing()`
The toExtendNothing() method may be used to ensure that all classes within a given namespace do not extend any class.
arch('app')
->expect('App\ValueObjects')
->toExtendNothing();`toImplement()`
The toImplement() method may be used to ensure that all classes within a given namespace implement a specific interface.
arch('app')
->expect('App\Jobs')
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');`toImplementNothing()`
The toImplementNothing() method may be used to ensure that all classes within a given namespace do not implement any interface.
arch('app')
->expect('App\ValueObjects')
->toImplementNothing();`toHaveMethodsDocumented()`
The toHaveMethodsDocumented() method may be used to ensure that all methods within a given namespace are documented.
arch('app')
->expect('App')
->toHaveMethodsDocumented();`toHavePropertiesDocumented()`
The toHavePropertiesDocumented() method may be used to ensure that all properties within a given namespace are documented.
arch('app')
->expect('App')
->toHavePropertiesDocumented();`toHaveAttribute()`
The toHaveAttribute() method may be used to ensure that a certain class has a specific attribute.
arch('app')
->expect('App\Console\Commands')
->toHaveAttribute('Symfony\Component\Console\Attribute\AsCommand');`toHaveFileSystemPermissions()`
The toHaveFileSystemPermissions() method may be used to ensure that all files within a given namespace have specific file system permissions.
arch('app')
->expect('App')
->not->toHaveFileSystemPermissions('0777');`toHaveLineCountLessThan()`
The toHaveLineCountLessThan() method may be used to ensure that all files within a given namespace have a line count less than a specified value.
arch('app')
->expect('App\Models')
->toHaveLineCountLessThan(100);`toHaveMethod()`
The toHaveMethod() method may be used to ensure that a certain class has a specific method.
arch('app')
->expect('App\Http\Controllers\HomeController')
->toHaveMethod('index');`toHaveMethods()`
The toHaveMethods() method may be used to ensure that a certain class has specific methods.
arch('app')
->expect('App\Http\Controllers\HomeController')
->toHaveMethods(['index', 'show']);`toHavePrivateMethodsBesides()`
The toHavePrivateMethodsBesides() method may be used to ensure that a certain class does not have any private methods besides the specified ones.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePrivateMethodsBesides(['doPayment']);`toHavePrivateMethods()`
The toHavePrivateMethods() method may be used to ensure that a certain class does not have any private methods.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePrivateMethods();`toHaveProtectedMethodsBesides()`
The toHaveProtectedMethodsBesides() method may be used to ensure that a certain class does not have any protected methods besides the specified ones.
arch('app')
->expect('App\Services\PaymentService')
->not->toHaveProtectedMethodsBesides(['doPayment']);`toHaveProtectedMethods()`
The toHaveProtectedMethods() method may be used to ensure that a certain class does not have any protected methods.
arch('app')
->expect('App\Services\PaymentService')
->not->toHaveProtectedMethods();`toHavePublicMethodsBesides()`
The toHavePublicMethodsBesides() method may be used to ensure that a certain class does not have any public methods besides the specified ones.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePublicMethodsBesides(['charge', 'refund']);`toHavePublicMethods()`
The toHavePublicMethods() method may be used to ensure that a certain class does not have any public methods.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePublicMethods();`toHavePrefix()`
The toHavePrefix() method may be used to ensure that all files within a given namespace have a specific prefix.
arch('app')
->expect('App\Helpers')
->not->toHavePrefix('Helper');`toHaveSuffix()`
The toHaveSuffix() method may be used to ensure that all files within a given namespace have a specific suffix.
arch('app')
->expect('App\Http\Controllers')
->toHaveSuffix('Controller');`toHaveConstructor()`
This toHaveConstructor() method may be used to ensure that all files within a given namespace have a __construct method.
arch('app')
->expect('App\ValueObjects')
->toHaveConstructor();`toHaveDestructor()`
This toHaveDestructor() method may be used to ensure that all files within a given namespace have a __destruct method.
arch('app')
->expect('App\ValueObjects')
->toHaveDestructor();`toOnlyImplement()`
The toOnlyImplement() method may be used to ensure that certain classes are restricted to implementing specific interfaces.
arch('app')
->expect('App\Responses')
->toOnlyImplement('Illuminate\Contracts\Support\Responsable');`toOnlyUse()`
The toOnlyUse() method may be used to guarantee that certain classes are restricted to utilizing specific functions or classes. For example, you may ensure your models are streamlined and solely dependent on the Illuminate\Database namespace, and not, for instance, dispatching queued jobs or events.
arch('models')
->expect('App\Models')
->toOnlyUse('Illuminate\Database');`toOnlyBeUsedIn()`
The toOnlyBeUsedIn() method enables you to limit the usage of a specific class or set of classes to only particular parts of your application. For instance, you can use this method to confirm that your models are only used by your repositories and not by controllers or service providers.
arch('models')
->expect('App\Models')
->toOnlyBeUsedIn('App\Repositories');`toUse()`
By combining the not modifier with the toUse() method, you can indicate that files within a given namespace should not use specific functions or classes.
arch('globals')
->expect('App\Domain')
->not->toUse('request');
arch('globals')
->expect('App\Domain')
->not->toUse('Illuminate\Http');`toUseStrictEquality()`
The toUseStrictEquality() method may be used to ensure that all files within a given namespace use strict equality. In other words, the === operator is used instead of the == operator.
arch('models')
->expect('App')
->toUseStrictEquality();Or, if you rather want to ensure that all files within a given namespace do not use strict equality, you may use the not modifier.
arch('models')
->expect('App')
->not->toUseStrictEquality();`toUseTrait()`
The toUseTrait() method may be used to ensure that all files within a given namespace use a specific trait.
arch('models')
->expect('App\Models')
->toUseTrait('Illuminate\Database\Eloquent\SoftDeletes');`toUseTraits()`
The toUseTraits() method may be used to ensure that all files within a given namespace use specific traits.
arch('models')
->expect('App\Models')
->toUseTraits(['Illuminate\Database\Eloquent\SoftDeletes', 'App\Concerns\CustomTrait']);`toUseNothing()`
If you want to indicate that particular namespaces or classes should not have any dependencies, you can utilize the toUseNothing() method.
arch('value objects')
->expect('App\ValueObjects')
->toUseNothing();`toUseStrictTypes()`
The toUseStrictTypes() method may be used to ensure that all files within a given namespace utilize strict types.
arch('app')
->expect('App')
->toUseStrictTypes();Presets
Sometimes, writing arch expectations from scratch can be time-consuming, specifically when working on a new project, and you just want to ensure that the basic architectural rules are met.
Presets are predefined sets of granular expectations that you can use to test your application's architecture.
`php`
The php preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It avoids the usage of die, var_dump, and similar functions, and ensures you are not using deprecated PHP functions.
arch()->preset()->php();You may find all the expectations included in the php preset below in our source code.
`security`
The security preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are not using code that could lead to security vulnerabilities, such as eval, md5, and similar functions.
arch()->preset()->security();You may find all the expectations included in the security preset below in our source code.
`laravel`
The laravel preset is a predefined set of expectations that can be used on Laravel projects.
It ensures you project's structure is following the well-known Laravel conventions, such as controllers only have index, show, create, store, edit, update, destroy as public methods and are always suffixed with Controller and so on.
arch()->preset()->laravel();You may find all the expectations included in the laravel preset below in our source code.
`strict`
The strict preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are using strict types in all your files, that all your classes are final, and more.
arch()->preset()->strict();You may find all the expectations included in the strict preset below in our source code.
`relaxed`
The relaxed preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It is the opposite of the strict preset, ensuring you are not using strict types in all your files, that all your classes are not final, and more.
arch()->preset()->relaxed();You may find all the expectations included in the relaxed preset below in our source code.
`custom`
Typically, you don't need to create a custom preset, as you can use the arch() method to write your granular expectations. However, if you want to create your own preset, you can use the custom method to define the preset.
This may be useful if you have a set of expectations that you use frequently across multiple projects, or if you are a plugin author and want to provide a set of expectations for your users.
pest()->presets()->custom('ddd', function () {
return [
expect('Infrastructure')->toOnlyBeUsedIn('Application'),
expect('Domain')->toOnlyBeUsedIn('Application'),
];
});Within the custom method, you may have access to the application PSR-4 namespaces on the first argument of your closure's callback.
pest()->presets()->custom('silex', function (array $userNamespaces) {
var_dump($userNamespaces); // array(1) { [0]=> string(3) "App" }
return [
expect($userNamespaces)->toBeArray(),
];
});You can then use the custom preset by chaining the preset() method with the name of the custom preset.
arch()->preset()->silex();Wildcards
Since Pest 3.8, you can pass wildcards to the expect() method to match code in multiple namespaces. For example, if you want to ensure all code within any Traits subdirectory contain traits, you can use the following:
arch()
->expect('App\*\Traits') // All code within any App\*\Traits namespace, e.g. App\Models\Traits, etc.
->toBeTraits();
arch()
->expect('App\*\*\Traits') // All code within any App\*\*\Traits namespace, e.g. App\A\B\Traits, App\C\D\Traits, etc.
->toBeTraits();Modifiers
Sometimes, you may want to apply the given expectation but excluding certain types of files, or ignoring certain classes, functions, or specific lines of code. For that, you may use the following methods:
`ignoring()`
When defining your architecture rules, you can use the ignoring() method to exclude certain namespaces or classes that would otherwise be included in the rule definition.
arch()
->preset()
->php()
->ignoring('die');
arch()
->expect('Illuminate\Support\Facades')
->not->toBeUsed()
->ignoring('App\Providers');In some cases, certain components may not be regarded as "dependencies" as they are part of the native PHP library. To customize the definition of "native" code and exclude it during testing, Pest allows you to specify what to ignore.
For example, if you do not want to consider Laravel a "dependency", you can use the arch() method inside the beforeEach() function to disregard any code within the "Illuminate" namespace. This approach allows you to focus only on the actual dependencies of your application.
// tests/Pest.php
pest()->beforeEach(function () {
$this->arch()->ignore([
'Illuminate',
])->ignoreGlobalFunctions();
});`classes()`
The classes() modifier allows you to restrict the expectation to only classes.
arch('app')
->expect('App')
->classes()
->toBeFinal();`enums()`
The enums() modifier allows you to restrict the expectation to only enums.
arch('app')
->expect('App\Models')
->enums()
->toOnlyBeUsedIn('App\Models');`interfaces()`
The interfaces() modifier allows you to restrict the expectation to only interfaces.
arch('app')
->expect('App')
->interfaces()
->toExtend('App\Contracts\Contract');`traits()`
The traits() modifier allows you to restrict the expectation to only traits.
arch('app')
->expect('App')
->traits()
->toExtend('App\Traits\Trait');`extending()`
The extending() modifier allows you to restrict the expectation to only classes or interfaces that extend the given class.
arch('app')
->expect('App')
->extending(Model::class)
->toUseTrait(HasFactory::class);`implementing()`
The implementing() modifier allows you to restrict the expectation to only classes that implement the given interface.
arch('app')
->expect('App')
->implementing(ShouldQueue::class)
->toUseTrait(Dispatchable::class);`using()`
The using() modifier allows you to restrict the expectation to only classes that use the given trait.
arch('app')
->expect('App')
->using(HasFactory::class)
->toExtend(Model::class);`abstracts()`
The abstracts() modifier allows you to restrict the expectation to only abstract classes.
arch('app')
->expect('App')
->abstracts()
->toImplement(JsonSerializable::class);In this section, you have learned how to perform architectural testing, ensuring that your application or library's architecture meets a specified set of architectural requirements. Next, have you ever wondered how to test the performance of your code? Let's explore Stress Testing.
File: browser-testing.md
title: Browser Testing
Browser Testing
Browser testing is an essential part of modern web development, allowing you to ensure that your application works correctly across different browsers and devices. Pest provides a simple and elegant way to write browser tests. Here is an example of how to write a browser test using Pest:
it('may welcome the user', function () {
$page = visit('/');
$page->assertSee('Welcome');
});This is a basic example of a browser test that checks if the homepage contains the text "Welcome". However, Pest's browser testing capabilities go beyond this simple example. You can use various methods to interact with the page, such as clicking buttons, filling out forms, and navigating between pages.
Here is an example of a more complex browser test, on Laravel, that checks if a user can sign in:
it('may sign in the user', function () {
Event::fake();
User::factory()->create([ // assumes RefreshDatabase trait is used on Pest.php...
'email' => '[email protected]',
'password' => 'password',
]);
$page = visit('/')->on()->mobile()->firefox();
$page->click('Sign In')
->assertUrlIs('/login')
->assertSee('Sign In to Your Account')
->fill('email', '[email protected]')
->fill('password', 'password')
->click('Submit')
->assertSee('Dashboard');
$this->assertAuthenticated();
Event::assertDispatched(UserLoggedIn::class);
});Note that, you are leveraging the full power of Laravel's testing capabilities, such as refresh database, event faking, and authentication assertions, while also actually doing browser testing.
Getting Started
To get started with browser testing in Pest, you need to install the Pest Browser plugin. You can do this by running the following command:
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright installFinally, add tests/Browser/Screenshots to your .gitignore file to avoid committing screenshots taken during browser tests.
Running Browser Tests
Running browser tests is similar to running regular Pest tests:
./vendor/bin/pestWe recommend running tests in parallel using the --parallel option to speed up the execution:
./vendor/bin/pest --parallelFor debugging purposes, you can run the tests in a headed mode and pause the execution at the end of the failed test run:
./vendor/bin/pest --debugNavigation
The visit() method is used to navigate to a specific URL in your browser test. It provides various methods to interact with the page:
test('example', function () {
$page = visit('/');
$page->assertSee('Welcome');
});Using Other Browsers
By default, the visit() method uses Chrome as the browser. However, if you want to use a different browser, you can specify it using the --browser option when running the tests:
./vendor/bin/pest --browser=firefox
./vendor/bin/pest --browser=safariIf you wish to use a different browser by default without specifying it in the command line, you can set it in your Pest.php configuration file:
pest()->browser()->inFirefox();
pest()->browser()->inSafari();Using Other Devices
The visit() method uses a desktop viewport. However, you can specify a mobile viewport using the onMobile() method. For example:
$page = visit('/')->on()->mobile();If you wish to use a specific device, you can use the on() method and chain it with the macbook14, iPhone14Pro, etc:
$page = visit('/')->on()->iPhone14Pro();Using Dark Mode
Pest enforces a light color scheme by default. However, you can specify a dark color scheme using the inDarkMode() method:
$page = visit('/')->inDarkMode();Visiting Multiple Pages
You can visit multiple pages simultaneously by passing an array of URLs to the visit() method. This is useful for testing scenarios where you need to interact with multiple pages at once:
$pages = visit(['/', '/about']);
$pages->assertNoSmoke()
->assertNoAccessibilityIssues()
->assertNoConsoleLogs()
->assertNoJavaScriptErrors();
[$homePage, $aboutPage] = $pages;
$homePage->assertSee('Welcome to our website');
$aboutPage->assertSee('About Us');Navigation
After visiting a page, you can navigate to other pages using the navigate() method. This method allows you to navigate to a different URL while keeping the current browser context:
$page = visit('/');
$page->navigate('/about')
->assertSee('About Us');Locating Elements
You can locate elements in the DOM using text or CSS selectors. Pest provides a simple syntax for locating elements:
// Clicks the first link with the text "Login"
$page->click('Login');
// Clicks the first element with the class "btn-primary"
$page->click('.btn-primary');
// Clicks the element with the data-test attribute "login"
$page->click('@login');
// Clicks the element with the ID "submit-button"
$page->click('#submit-button');
// etc...Configuring Timeouts
Sometimes, elements may take time to appear on the page. By default, Pest waits for 5 seconds before timing out. You can configure the default timeout for browser tests in your Pest.php configuration file:
pest()->browser()->timeout(10);Table of Contents
Available Assertions
assertTitle
assertTitleContains
assertSee
assertDontSee
assertSeeIn
assertDontSeeIn
assertSeeAnythingIn
assertSeeNothingIn
assertCount
assertScript
assertSourceHas
assertSourceMissing
assertSeeLink
assertDontSeeLink
assertChecked
assertNotChecked
assertIndeterminate
assertRadioSelected
assertRadioNotSelected
assertSelected
assertNotSelected
assertValue
assertValueIsNot
assertAttribute
assertAttributeMissing
assertAttributeContains
assertAttributeDoesntContain
assertAriaAttribute
assertDataAttribute
assertVisible
assertPresent
assertNotPresent
assertMissing
assertEnabled
assertDisabled
assertButtonEnabled
assertButtonDisabled
assertUrlIs
assertSchemeIs
assertSchemeIsNot
assertHostIs
assertHostIsNot
assertPortIs
assertPortIsNot
assertPathBeginsWith
assertPathEndsWith
assertPathContains
assertPathIs
assertPathIsNot
assertQueryStringHas
assertQueryStringMissing
assertFragmentIs
assertFragmentBeginsWith
assertFragmentIsNot
assertNoSmoke
assertNoConsoleLogs
assertNoJavaScriptErrors
assertNoAccessibilityIssues
assertScreenshotMatches
Element Interactions
click
text
attribute
keys
type
select
append
clear
radio
check
uncheck
attach
press
pressAndWaitFor
drag
hover
submit
value
script
content
url
wait
waitForKey
Debugging tests
Element Assertions
assertTitle
The assertTitle method asserts that the page title matches the given text:
$page->assertTitle('Home Page');assertTitleContains
The assertTitleContains method asserts that the page title contains the given text:
$page->assertTitleContains('Home');assertSee
The assertSee method asserts that the given text is present on the page:
$page->assertSee('Welcome to our website');assertDontSee
The assertDontSee method asserts that the given text is not present on the page:
$page->assertDontSee('Error occurred');assertSeeIn
The assertSeeIn method asserts that the given text is present within the selector:
$page->assertSeeIn('.header', 'Welcome');assertDontSeeIn
The assertDontSeeIn method asserts that the given text is not present within the selector:
$page->assertDontSeeIn('.error-container', 'Error occurred');assertSeeAnythingIn
The assertSeeAnythingIn method asserts that any text is present within the selector:
$page->assertSeeAnythingIn('.content');assertSeeNothingIn
The assertSeeNothingIn method asserts that no text is present within the selector:
$page->assertSeeNothingIn('.empty-container');assertCount
The assertCount method asserts that a given element is present a given amount of times:
$page->assertCount('.item', 5);assertScript
The assertScript method asserts that the given JavaScript expression evaluates to the given value:
$page->assertScript('document.title', 'Home Page');
$page->assertScript('document.querySelector(".btn").disabled', true);assertSourceHas
The assertSourceHas method asserts that the given source code is present on the page:
$page->assertSourceHas('<h1>Welcome</h1>');assertSourceMissing
The assertSourceMissing method asserts that the given source code is not present on the page:
$page->assertSourceMissing('<div class="error">');assertSeeLink
The assertSeeLink method asserts that the given link is present on the page:
$page->assertSeeLink('About Us');assertDontSeeLink
The assertDontSeeLink method asserts that the given link is not present on the page:
$page->assertDontSeeLink('Admin Panel');assertChecked
The assertChecked method asserts that the given checkbox is checked:
$page->assertChecked('terms');
$page->assertChecked('color', 'blue'); // For checkbox with specific valueassertNotChecked
The assertNotChecked method asserts that the given checkbox is not checked:
$page->assertNotChecked('newsletter');
$page->assertNotChecked('color', 'red'); // For checkbox with specific valueassertIndeterminate
The assertIndeterminate method asserts that the given checkbox is in an indeterminate state:
$page->assertIndeterminate('partial-selection');assertRadioSelected
The assertRadioSelected method asserts that the given radio field is selected:
$page->assertRadioSelected('size', 'large');assertRadioNotSelected
The assertRadioNotSelected method asserts that the given radio field is not selected:
$page->assertRadioNotSelected('size', 'small');assertSelected
The assertSelected method asserts that the given dropdown has the given value selected:
$page->assertSelected('country', 'US');assertNotSelected
The assertNotSelected method asserts that the given dropdown does not have the given value selected:
$page->assertNotSelected('country', 'UK');assertValue
The assertValue method asserts that the element matching the given selector has the given value:
$page->assertValue('input[name=email]', '[email protected]');assertValueIsNot
The assertValueIsNot method asserts that the element matching the given selector does not have the given value:
$page->assertValueIsNot('input[name=email]', '[email protected]');assertAttribute
The assertAttribute method asserts that the element matching the given selector has the given value in the provided attribute:
$page->assertAttribute('img', 'alt', 'Profile Picture');assertAttributeMissing
The assertAttributeMissing method asserts that the element matching the given selector is missing the provided attribute:
$page->assertAttributeMissing('button', 'disabled');assertAttributeContains
The assertAttributeContains method asserts that the element matching the given selector contains the given value in the provided attribute:
$page->assertAttributeContains('div', 'class', 'container');assertAttributeDoesntContain
The assertAttributeDoesntContain method asserts that the element matching the given selector does not contain the given value in the provided attribute:
$page->assertAttributeDoesntContain('div', 'class', 'hidden');assertAriaAttribute
The assertAriaAttribute method asserts that the element matching the given selector has the given value in the provided aria attribute:
$page->assertAriaAttribute('button', 'label', 'Close');assertDataAttribute
The assertDataAttribute method asserts that the element matching the given selector has the given value in the provided data attribute:
$page->assertDataAttribute('div', 'id', '123');assertVisible
The assertVisible method asserts that the element matching the given selector is visible:
$page->assertVisible('.alert');assertPresent
The assertPresent method asserts that the element matching the given selector is present in the DOM:
$page->assertPresent('form');assertNotPresent
The assertNotPresent method asserts that the element matching the given selector is not present in the DOM:
$page->assertNotPresent('.error-message');assertMissing
The assertMissing method asserts that the element matching the given selector is not visible:
$page->assertMissing('.hidden-element');assertEnabled
The assertEnabled method asserts that the given field is enabled:
$page->assertEnabled('email');assertDisabled
The assertDisabled method asserts that the given field is disabled:
$page->assertDisabled('submit');assertButtonEnabled
The assertButtonEnabled method asserts that the given button is enabled:
$page->assertButtonEnabled('Save');assertButtonDisabled
The assertButtonDisabled method asserts that the given button is disabled:
$page->assertButtonDisabled('Submit');URL Assertions
assertUrlIs
The assertUrlIs method asserts that the current URL matches the given string:
$page->assertUrlIs('https://example.com/home');assertSchemeIs
The assertSchemeIs method asserts that the current URL scheme matches the given scheme:
$page->assertSchemeIs('https');assertSchemeIsNot
The assertSchemeIsNot method asserts that the current URL scheme does not match the given scheme:
$page->assertSchemeIsNot('http');assertHostIs
The assertHostIs method asserts that the current URL host matches the given host:
$page->assertHostIs('example.com');assertHostIsNot
The assertHostIsNot method asserts that the current URL host does not match the given host:
$page->assertHostIsNot('wrong-domain.com');assertPortIs
The assertPortIs method asserts that the current URL port matches the given port:
$page->assertPortIs('443');assertPortIsNot
The assertPortIsNot method asserts that the current URL port does not match the given port:
$page->assertPortIsNot('8080');assertPathBeginsWith
The assertPathBeginsWith method asserts that the current URL path begins with the given path:
$page->assertPathBeginsWith('/users');assertPathEndsWith
The assertPathEndsWith method asserts that the current URL path ends with the given path:
$page->assertPathEndsWith('/profile');assertPathContains
The assertPathContains method asserts that the current URL path contains the given path:
$page->assertPathContains('settings');assertPathIs
The assertPathIs method asserts that the current path matches the given path:
$page->assertPathIs('/dashboard');assertPathIsNot
The assertPathIsNot method asserts that the current path does not match the given path:
$page->assertPathIsNot('/login');assertQueryStringHas
The assertQueryStringHas method asserts that the given query string parameter is present and has a given value:
$page->assertQueryStringHas('page');
$page->assertQueryStringHas('page', '2');assertQueryStringMissing
The assertQueryStringMissing method asserts that the given query string parameter is missing:
$page->assertQueryStringMissing('page');assertFragmentIs
The assertFragmentIs method asserts that the URL's current hash fragment matches the given fragment:
$page->assertFragmentIs('section-2');assertFragmentBeginsWith
The assertFragmentBeginsWith method asserts that the URL's current hash fragment begins with the given fragment:
$page->assertFragmentBeginsWith('section');assertFragmentIsNot
The assertFragmentIsNot method asserts that the URL's current hash fragment does not match the given fragment:
$page->assertFragmentIsNot('wrong-section');Console Assertions
assertNoSmoke
The assertNoSmoke method asserts there are no console logs or JavaScript errors on the page:
$page->assertNoSmoke();assertNoConsoleLogs
The assertNoConsoleLogs method asserts there are no console logs on the page:
$page->assertNoConsoleLogs();assertNoJavaScriptErrors
The assertNoJavaScriptErrors method asserts there are no JavaScript errors on the page:
$page->assertNoJavaScriptErrors();assertNoAccessibilityIssues
The assertNoAccessibilityIssues method asserts there are no "serious" accessibility issues on the page:
$page->assertNoAccessibilityIssues();By default, the level is 1 (serious). You can change to one of the following levels:
0. Critical
1. Serious
2. Moderate
3. Minor- The level 0 (critical) only reports issues that cause severe barriers for individuals with disabilities. The organization may be subject to legal action if these issues are not addressed.
- The level 1 (serious) includes all critical issues (level 0) and adds issues that significantly impact accessibility. The organization may be subject to legal action if these issues are not addressed.
- The level 2 (moderate) includes all serious issues (level 1) and adds issues that moderately affect accessibility. The end-user would appreciate the fix, but it is not a barrier.
- The level 3 (minor) includes all moderate issues (level 2) and adds issues that have a minor impact on accessibility. These issues are often related to best practices and do not significantly affect the user experience.
Screenshot Assertions
assertScreenshotMatches
The assertScreenshotMatches method asserts that the screenshot matches the expected image:
$page->assertScreenshotMatches();
$page->assertScreenshotMatches(true, true); // Full page, show diffElement Interactions
click
The click method clicks the link with the given text:
$page->click('Login');text
The text method gets the text of the element matching the given selector:
$text = $page->text('.header');attribute
The attribute method gets the given attribute from the element matching the given selector:
$alt = $page->attribute('img', 'alt');keys
The keys method sends the given keys to the element matching the given selector:
$page->keys('input[name=password]', 'secret');
$page->keys('input[name=password]', ['{Control}', 'a']); // Keyboard shortcutstype
The type method types the given value in the given field:
$page->type('email', '[email protected]');select
The select method selects the given value in the given field:
$page->select('country', 'US');
$page->select('interests', ['music', 'sports']); // Multiple selectappend
The append method types the given value in the given field without clearing it:
$page->append('description', ' Additional information.');clear
The clear method clears the given field:
$page->clear('search');radio
The radio method selects the given value of a radio button field:
$page->radio('size', 'large');check
The check method checks the given checkbox:
$page->check('terms');
$page->check('color', 'blue'); // For checkbox with specific valueuncheck
The uncheck method unchecks the given checkbox:
$page->uncheck('newsletter');
$page->uncheck('color', 'red'); // For checkbox with specific valueattach
The attach method attaches the given file to the field:
$page->attach('avatar', '/path/to/image.jpg');press
The press method presses the button with the given text or name:
$page->press('Submit');pressAndWaitFor
The pressAndWaitFor method presses the button with the given text or name and waits for a specified amount of time:
$page->pressAndWaitFor('Submit', 2); // Wait for 2 secondsdrag
The drag method drags an element to another element using selectors:
$page->drag('#item', '#target');hover
The hover method hovers over the given element:
$page->hover('#item');submit
The submit method submits the first form found on the page:
$page->submit();value
The value method gets the value of the element matching the given selector:
$value = $page->value('input[name=email]');script
The script method executes a script in the context of the page:
$result = $page->script('document.title');content
The content method gets the page's content:
$html = $page->content();url
The url method gets the page's URL:
$currentUrl = $page->url();wait
The wait method pauses for the given number of seconds:
$page->wait(2); // Wait for 2 secondswaitForKey
The waitForKey method opens the current page URL in the default web browser and waits for a key press:
$page->waitForKey(); // Useful for debuggingDebugging tests
Sometimes you may want to debug your browser tests. Pest provides a convenient way to do this by using the --debug option, which makes pest to open the browser window and pause the execution of the test when it fails. You can then inspect the page and see what went wrong.
./vendor/bin/pest --debugOptionally, you can also use the debug() method in your test to pause the execution and open the browser window:
$page->debug();
You can also take a screenshot of the current page using the screenshot() method. This is useful for visual debugging:
$page->screenshot();
$page->screenshot(fullPage: true);
$page->screenshot(filename: 'custom-name');
You can also use the tinker() method to open a Tinker session in the context of the current page. This allows you to interact with the page using PHP code:
$page->tinker();
After you can run your tests with the --headed option to open the browser window:
./vendor/bin/pest --headedIf you wish to run the tests in a headed mode by default, you can set it in your Pest.php configuration file:
pest()->browser()->headed();Continuous Integration
You may refer to Pest's Continuous Integration documentation for more information on how to run your browser tests in a CI environment.
However, if you are using GitHub Actions, you need to add the following steps to your workflow file:
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-depsNow, let's dive into architectural testing and how it can benefit your development process. By performing architectural testing, you can evaluate the overall design of your application and identify potential flaws before they become significant issues: Arch Testing
File: cli-api-reference.md
title: CLI API Reference
CLI API Reference
In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.
Configuration
--init: Initialize a standard Pest configuration.--bootstrap <file>A PHP script that is included before the tests run.-c|--configuration <file>: Read configuration from XML file.--no-configuration: Ignore default configuration file (phpunit.xml).--extension <class>: Register test runner extension with bootstrap. --no-extensions: Do not load PHPUnit extensions.--include-path: Prepend PHP's include_path with given path(s).-d <key[=value]>: Set a php.ini value.--cache-directory <dir>: Specify cache directory.--generate-configuration: Generate configuration file with suggested settings.--migrate-configuration: Migrate configuration file to current format.--generate-baseline <file>: Generate baseline for issues.--use-baseline <file>: Use baseline to ignore issues.--ignore-baseline: Do not use baseline to ignore issues.--test-directory: Specify test directory containing Pest.php, TestCase.php, helpers and your tests. Default: tests
Selection
--bail: Stop execution upon first not-passed test.--ci: Ignore focused tests using->only()and run the entire test suite.--todos: Output to standard output the list of todos.--notes: Output to standard output tests with notes.--issue: Output to standard output tests with the given issue number.--pr: Output to standard output tests with the given pull request number.--pull-request: Output to standard output tests with the given pull request number (alias for--pr).--retry: Run non-passing tests first and stop execution upon first error or failure.--list-suitesList available test suites.--testsuite <name>: Only run tests from the specified test suite(s).--exclude-testsuite <name>: Exclude tests from the specified test suite(s).--list-groups: List available test groups.--group <name>: Only run tests from the specified group(s).--exclude-group <name>: Exclude tests from the specified group(s).--covers <name>: Only run tests that intend to cover<name>.--uses <name>: Only run tests that intend to use<name>.--requires-php-extension <name>: Only run tests that require PHP extension. --list-test-files: List available test files.--list-tests: List available tests.--list-tests-xml <file>: List available tests in XML format.--filter: Filter which tests to run--exclude-filter: Exclude tests for the specified filter pattern.--test-suffix <suffixes>: Only search for test in files with specified suffix(es). Default: Test.php,.phpt
Execution
--parallelRun tests in parallel.--update-snapshots: Update snapshots for tests using the "toMatchSnapshot" expectation.--globals-backup: Backup and restore $GLOBALS for each test.--static-backup: Backup and restore static properties for each test.--strict-coverage: Be strict about code coverage metadata.--strict-global-state: Be strict about changes to global state.--disallow-test-output: Be strict about output during tests.--enforce-time-limit: Enforce time limit based on test size.--default-time-limit <sec>: Timeout in seconds for tests that have no declared size.--dont-report-useless-tests: Do not report tests that do not test anything.--stop-on-defect: Stop execution upon first not-passed test.--stop-on-error: Stop execution upon first error.--stop-on-failure: Stop execution upon first error or failure.--stop-on-warning: Stop execution upon first warning.--stop-on-risky: Stop execution upon first risky test.--stop-on-deprecation: Stop after first test that triggered a deprecation.--stop-on-notice: Stop after first test that triggered a notice.--stop-on-skipped: Stop execution upon first skipped test.--stop-on-incomplete: Stop execution upon first incomplete test.--fail-on-empty-test-suite: Signal failure using shell exit code when no tests were run.--fail-on-warning: Treat tests with warnings as failures.--fail-on-risky: Treat risky tests as failures.--fail-on-deprecation: Signal failure using shell exit code when a deprecation was triggered.--fail-on-phpunit-deprecation: Signal failure using shell exit code when a PHPUnit deprecation was triggered.--fail-on-notice: Signal failure using shell exit code when a notice was triggered.--fail-on-skipped: Treat skipped tests as failures.--fail-on-incomplete: Signal failure using shell exit code when a test was marked incomplete.--cache-result: Write test results to cache file.--do-not-cache-result: Do not write test results to cache file--order-by <order>: Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size.--random-order-seed <N>: Use the specified random seed when running tests in random order
Reporting
--colors <flag>: Use colors in output ("never", "auto" or "always").--columns <n>: Number of columns to use for progress output.--columns max: Use maximum number of columns for progress output.--stderr: Write to STDERR instead of STDOUT.--no-progress: Disable output of test execution progress.--no-results: Disable output of test results.--no-output: Disable all output.--display-incomplete: Display details for incomplete tests.--display-skipped: Display details for skipped tests.--display-deprecations: Display details for deprecations triggered by tests.--display-phpunit-deprecations: Display details for PHPUnit deprecations.--display-errors: Display details for errors triggered by tests.--display-notices: Display details for notices triggered by tests.--display-warnings: Display details for warnings triggered by tests.--reverse-list: Print defects in reverse order.--teamcity: Replace default progress and result output with TeamCity format.--testdox: Replace default result output with TestDox format--testdox-summary: Repeat TestDox output for tests with errors, failures, or issues.--debug: Replace default progress and result output with debugging information.--compact: Replace default result output with Compact format
Logging
--log-junit <file>: Write test results in JUnit XML format to file.--log-teamcity <file>: Write test results in TeamCity format to file.--testdox-html <file>: Write test results in TestDox format (HTML) to file.--testdox-text <file>: Write test results in TestDox format (plain text) to file.--log-events-text <file>: Stream events as plain text to file.--log-events-verbose-text <file>: Stream events as plain text (with telemetry information) to file.--no-logging: Ignore logging configured in the XML configuration file
Code Coverage
--coverage: Generate code coverage report and output to standard output.--coverage --min=<value>: Set the minimum required coverage percentage, and fail if not met.--coverage-clover <file>: Write code coverage report in Clover XML format to file.--coverage-cobertura <file>: Write code coverage report in Cobertura XML format to file.--coverage-crap4j <file>: Write code coverage report in Crap4J XML format to file.--coverage-html <dir>: Write code coverage report in HTML format to directory.--coverage-php <file>: Write serialized code coverage data to file.--coverage-text=<file>: Write code coverage report in text format to file [default: standard output].--only-summary-for-coverage-text: Option for code coverage report in text format: only show summary.--show-uncovered-for-coverage-text: Option for code coverage report in text format: show uncovered files.--coverage-xml <dir>: Write code coverage report in XML format to directory.--warm-coverage-cache: Warm static analysis cache.--coverage-filter <dir>: Include<dir>: in code coverage reporting.--path-coverage: Report path coverage in addition to line coverage.--disable-coverage-ignore: Disable metadata for ignoring code coverage.--no-coverage: Ignore code coverage reporting configured in the XML configuration file
Mutation Testing
--mutate: Runs mutation testing, to understand the quality of your tests.--mutate --parallel: Runs mutation testing in parallel.--mutate --min: Set the minimum required mutation score, and fail if not met.--mutate --id: Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run.--mutate --covered-only: Only generate mutations for classes that are covered by tests.--mutate --bail: Stop mutation testing execution upon first untested or uncovered mutation.--mutate --class: Generate mutations for the given class(es). E.g. --class=App\Models.--mutate --ignore: Ignore the given class(es) when generating mutations. E.g. --ignore=App\Http\Requests.--mutate --clear-cache: Clear the mutation cache.--mutate --no-cache: Clear the mutation cache.--mutate --ignore-min-score-on-zero-mutations: Ignore the minimum score requirement when there are no mutations.--mutate --covered-only: Only generate mutations for classes that are covered by tests.--mutate --everything: Generate mutations for all classes, even if they are not covered by tests.--mutate --profile: Output to standard output the top ten slowest mutations.--mutate --retry: Run untested or uncovered mutations first and stop execution upon first error or failure.--mutate --stop-on-uncovered: Stop mutation testing execution upon first untested mutation.--mutate --stop-on-untested: Stop mutation testing execution upon first untested mutation.
Profiling
--profile: Output to standard output the top ten slowest tests
In this chapter, you found a complete list of CLI options provided by Pest. In the subsequent documentation, we will explore the topic of test dependencies: Test Dependencies
File: community-guide.md
title: Community Guide
Community Guide
Our project aims to develop the world's finest testing framework that not only establishes itself as the standard choice in the PHP ecosystem, but also serves as a catalyst for change and inspiration in other ecosystems.
Your contribution is crucial to achieving our ambitious goal. We strongly believe that the PHP ecosystem has the talent and work ethic necessary to accomplish this goal. In the following section, we will outline the various areas where you can lend your assistance and become an integral part of our mission.
Develop Educational Resources: There is a popular saying that the best way to learn is to teach. If you have something interesting to share about your experience with Pest, you can reinforce your knowledge by creating a blog post, conducting a workshop, creating a video, or even publishing a gist.
Help Fellow Users: It's important to remember that contributing to the Pest's growth goes beyond just writing code. Providing assistance to other Pest users is a valuable form of contribution as well. At this time, we are present on Discord and Telegram. However, you are free to create other community channels.
Discord: discord.gg/kaHY6p54JH
Telegram: t.me/+kYH5G4d5MV83ODk0
Improve Our Documentation: If you have strong writing skills, you can assist us in enhancing Pest's documentation and coding examples. Simply navigate to a documentation page and click on the "Edit this page →" option located on the top right to get started.
Pest Documentation Repository: github.com/pestphp/docs
Speak At Meetups / Conferences: Delivering a talk or workshop can be a great way to contribute to the growth of Pest. You need not necessarily prepare something entirely from scratch, as there are already several conference talks on Pest available on YouTube that you can use as inspiration for your own talk.
Become a Community Leader: By becoming a testing (or Pest) advocate, you can increase its reach and impact. Start by sharing testing tips on social media platforms like Twitter and LinkedIn, and you may be surprised by the significant impact it can have on Pest's growth.
Twitter: @pestphp
Become a Code Contributor: If you have ideas for improvements or new features that can be introduced in Pest, you're welcome to share them on the Pest repository's GitHub issues board or GitHub discussion board. If you propose a new feature, please consider contributing some of the code needed to implement it. Please keep in mind that discussions regarding Pest development, including bugs, new features, and related topics, take place on GitHub, not through email or Twitter DMs.
Pest GitHub Repository: github.com/pestphp/pest
File: configuring-tests.md
title: Configuring Tests
Configuring Tests
The Pest.php file is a configuration file that is used to define your test suite setup. This file is located in the tests directory of your project and is automatically loaded by Pest when you run your tests. Although you can define Global Hooks or Custom Expectations within this file, its primary purpose is to specify the base test class utilized in your test suite.
When using Pest, the $this variable available within closures provided to test functions is bound to a specific test case class, which is typically PHPUnit\Framework\TestCase. This guarantees that the test cases written in Pest's functional style can access the underlying assertion API of PHPUnit, simplifying collaboration with other developers who are more familiar with the PHPUnit testing framework.
it('has home', function () {
echo get_class($this); // \PHPUnit\Framework\TestCase
$this->assertTrue(true);
});However, you may associate a specific folder or even your entire test suite with another base test case class, thus changing the value of $this within tests. To accomplish this, you can utilize the pest() function and the in() method within your Pest.php configuration file.
// tests/Pest.php
pest()->extend(Tests\TestCase::class)->in('Feature');
// tests/Feature/ExampleTest.php
it('has home', function () {
echo get_class($this); // \Tests\TestCase
});Additionally, Pest supports glob patterns in the in() method. This allows you to specify multiple directories or files with a single pattern. Glob patterns are string representations that can match various file paths, like wildcards. If you are unfamiliar with glob patterns, refer to the PHP manual here.
// tests/Pest.php
pest()->extend(Tests\TestCase::class)->in('Feature/*Job*.php');
// This will apply the Tests\TestCase to all test files in the "Feature" directory that contains "Job" in their filename.Another more complex example would be using a pattern to match multiple directories in different modules while applying multiple test case classes and traits:
// tests/Pest.php
pest()
->extend(DuskTestCase::class)
->use(DatabaseMigrations::class)
->in('../Modules/*/Tests/Browser');
// This will apply the DuskTestCase class and the DatabaseMigrations trait to all test files within any module's "Browser" directory.Any method that is defined as public or protected in your base test case class can be accessed within the test closure.
use PHPUnit\Framework\TestCase as BaseTestCase;
// tests/TestCase.php
class TestCase extends BaseTestCase
{
public function performThis(): void
{
//
}
}
// tests/Pest.php
pest()->extend(TestCase::class)->in('Feature');
// tests/Feature/ExampleTest.php
it('has home', function () {
$this->performThis();
});A trait can be linked to a test or folder, much like classes. For instance, in Laravel, you can employ the RefreshDatabase trait to reset the database prior to each test. To include the trait in your test, pass the trait's name to the pest()->use() method.
<?php
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature');To associate a particular test with a specific test case class or trait, you can utilize the pest()->extend() and pest()->use() methods within that specific test file, omitting the use of the in() method.
pest()->extend(Tests\MySpecificTestCase::class);
it('has home', function () {
echo get_class($this); // \Tests\MySpecificTestCase
});Next, one of the features available to you when configuring your test suite is the ability to group folders. When utilized, this feature allows you to filter your executed tests using the --group option: Grouping Tests
File: continuous-integration.md
title: Continuous Integration
Continuous Integration
Up until now, we have only discussed running tests from the command line on your local machine. But, you can also run your tests from a CI platform of your choice. As pestphp/pest is included in your Composer development dependencies, you can easily execute the vendor/bin/pest --ci command within your CI platform's deployment pipeline.
Example With GitHub Actions
If your application uses GitHub Actions as its CI platform, the following guidelines will assist you in configuring Pest so that your application is automatically tested when someone pushes a commit to your GitHub repository.
To get started, create a tests.yml file within the your-project/.github/workflows directory. The file should have the following contents:
name: Tests
on: ['push', 'pull_request']
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
tools: composer:v2
coverage: xdebug
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Tests
run: ./vendor/bin/pest --ciNaturally, you may customize the script above according to your requirements. For example, you may need to set up a database if your tests require one.
Once you have created your tests.yml file, commit and push the tests.yml file so GitHub Actions can run your tests. Keep in mind that once you make this commit, your test suite will execute on all new pull requests and commits.
Using Browser Testing with GitHub Actions
If you want to use Browser Testing with GitHub Actions, be sure to add a step that installs Playwright before running your tests. Here is an example of how to do this:
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Browser Tests
run: ./vendor/bin/pest --ci --parallelNote: Be sure to run your browser tests in parallel to speed up the execution time. You can do this by adding the
--parallelflag to the Pest command.
Example With GitLab CI/CD Pipelines
If your application uses GitLab CI/CD Pipelines as its CI platform, the following guidelines will assist you in configuring Pest so that your application is automatically tested when someone pushes a commit to your GitLab repository.
To get started, add the following configuration to your .gitlab-ci.yml file. The file should have the following contents:
stages:
- build
- test
build:vendors:
stage: build
only:
refs:
- merge_requests
- push
cache:
key:
files:
- composer.lock
policy: pull-push
image: composer:2
script:
- composer install --no-interaction --prefer-dist --optimize-autoloader
tests:
stage: test
only:
refs:
- merge_requests
- push
cache:
key:
files:
- composer.lock
policy: pull
image: php:8.2
script:
- ./vendor/bin/pest --ciNaturally, you may customize the script above according to your requirements. For example, you may need to set up a database if your tests require one.
Once you have created your .gitlab-ci.yml file, commit and push the .gitlab-ci.yml file so Gitlab CI/CD Pipelines can run your tests. Keep in mind that once you make this commit, your test suite will execute on all new merge requests and commits.
Example with Bitbucket Pipelines
If your application uses Bitbucket CI/CD Pipelines as its CI platform, the following guidelines will assist you in configuring Pest so that your application is automatically tested when someone pushes a commit to your Bitbucket repository.
To get started, add the following configuration to your bitbucket-pipelines.yml file. The file should have the following contents:
image: composer:2
pipelines:
default:
- parallel:
- step:
name: Test
script:
- composer install --no-interaction --prefer-dist --optimize-autoloader
- ./vendor/bin/pest
caches:
- composerNaturally, you may customize the script above according to your requirements. For example, you may need to set up a database if your tests require one.
Once you have created your bitbucket-pipelines.yml file, commit and push the bitbucket-pipelines.yml file so Bitbucket Pipelines can run your tests. Keep in mind that once you make this commit, your test suite will execute on all new pull requests and commits.
Example with Chipper CI
If your application uses Chipper CI as its CI platform, the following guidelines will assist you in configuring Pest so that your application is automatically tested when someone pushes a commit to your git repository.
To get started, add the following configuration to your .chipperci.yml file. The file should have the following contents:
version: 1
environment:
php: 8.3
node: 16
# Optional services
services:
# - mysql: 8
# - redis:
# Build all commits
on:
push:
branches: .*
pipeline:
- name: Setup
cmd: |
cp -v .env.example .env
composer install --no-interaction --prefer-dist --optimize-autoloader
php artisan key:generate
- name: Compile Assets
cmd: |
npm ci --no-audit
npm run build
- name: Test
cmd: pestIn addition to handling Composer and NPM caches, Chipper CI automatically adds vendor/bin to your PATH, so simply running the pest --ci command will work when running tests.
Naturally, you may customize the scripts above according to your requirements. For example, you may need to define a database service if your tests require one.
Once you have created your .chipperci.yml file, commit and push the .chipperci.yml file so Chipper CI can run your tests. Keep in mind that once you make this commit, your test suite will execute on all new commits.
Sharding Your Tests
If you have a large test suite, you may want to consider sharding your tests across multiple CI jobs to speed up the execution time. Pest supports test sharding out of the box, allowing you to split your tests into smaller groups that can be run in parallel.
To shard your tests, you can use the --shard option when running Pest. For example, if you want to run the first shard of your tests, you can use the following command:
./vendor/bin/pest --shard=1/5To implement test sharding in your CI configuration, you can create multiple jobs that run different shards of your test suite. Here is an example of how to do this with GitHub Actions:
strategy:
matrix:
shard: [1, 2, 3, 4, 5]
name: Tests (Shard ${{ matrix.shard }}/5)
steps:
- name: Run tests
run: pest --parallel --shard ${{ matrix.shard }}/5This configuration will create five jobs, each running a different shard of your test suite. You can adjust the number of shards based on the size of your test suite and the resources available in your CI environment.
Great job setting up Continuous Integration for your project to ensure codebase stability! Now, let's take a deeper dive into Pest's concepts by exploring it's test configuration capabilities: Configuring Pest →
File: creating-plugins.md
title: Creating Plugins
Creating Plugins
Community plugins are essential for offering additional features to the Pest community, while the Pest team prioritizes keeping the framework's core small and fast. In this chapter, we'll discuss how to create your own plugins and share them with the community.
The simplest way to develop your own plugin is to begin with the pest-plugin-template. To generate a new repository from the template repository, click on GitHub's "Use this template" button and name your new repository "pest-plugin-
After cloning the repository, make sure to modify the "name" and "description" fields in the composer.json file to suit your plugin.
Pest plugins are capable of exposing custom test methods via the $this variable, adding namespaced functions, defining custom expectations, and offering custom CLI options.
Adding Methods
Let's start building our plugin by making new test methods available via the $this variable. To accomplish this, define a PHP trait in your plugin.
namespace YourGitHubUsername\PestPluginName;
trait MyPluginTrait
{
public function myPluginMethod()
{
//
}
}In order to make this trait method invokable via Pest, we must inform Pest that it should make it available. This can be accomplished creating an Autoload.php file within your plugin with the following content.
use YourGitHubUsername\PestPluginName\MyPluginTrait;
Pest\Plugin::uses(MyPluginTrait::class);Lastly, we need to update our plugin's composer.json file to load our Autoload.php file as well as our plugin source code.
"autoload": {
"psr-4": {
"YourGitHubUsername\\PestPluginName\\": "src/"
},
"files": ["src/Autoload.php"]
},After you publish your plugin to Packagist, users will be able to install your plugin via Composer. Once installed, they will be able to access your plugin's functions within their test closures.
test('plugin example', function () {
$this->myPluginMethod();
//
})Adding Functions
A plugin can also define additional namespaced functions, which are typically declared within the plugin's Autoload.php file.
namespace YourGitHubUsername\PestPluginName;
function myPluginFunction(): void
{
//
}Within your plugins functions, you can access the current $this variable that would typically be available to test closures by invoking the test() function with no arguments.
namespace YourGitHubUsername\PestPluginName;
use PHPUnit\Framework\TestCase;
function myPluginFunction(): TestCase
{
return test(); // Same as `return $this;`
}Once you modify your plugin's composer.json file to autoload the Autoload.php file, users can easily access your function within their tests.
use function YourGitHubUsername\PestPluginName\{myPluginFunction};
test('plugin example', function () {
myPluginFunction();
// ...
}Adding Custom Expectations
Custom expectations can be incorporated into your plugin's Autoload.php file. For information on how to build custom expectations, please refer to the comprehensive documentation on Custom Expectations.
Adding Arch Presets
If your plugin provides a custom Arch preset, you can define it within the Autoload.php file.
pest()->preset('ddd', function () {
return [
expect('Infrastructure')->toOnlyBeUsedIn('Application'),
expect('Domain')->toOnlyBeUsedIn('Application'),
];
});Optionally, may have access to the application PSR-4 namespaces on the first argument of your closure's callback.
pest()->preset('silex', function (array $userNamespaces) {
dump($userNamespaces); // ['App\\']
});As you can see, crafting plugins on Pest can serve as a fantastic starting point for your open-source endeavors! In the next chapter, we will explore the concept of "Higher Order Testing": Higher Order Testing
File: custom-expectations.md
title: Custom Expectations
Custom Expectations
Pest's expectation API is powerful by default, but there may be times when you need to write the same expectations repeatedly between tests. In such cases, creating custom expectations that meet your specific requirements can be incredibly useful.
Custom expectations are usually defined in the tests/Pest.php file, but you can also organize them in a separate tests/Expectations.php file for better maintainability. To create a custom expectation in Pest, chain the extend() method onto the expect() function without providing any expectation value.
For example, suppose you are testing a number utility library and you need to frequently assert that numbers fall within a given range. In this case, you might create a custom expectation called toBeWithinRange():
// Pest.php or Expectations.php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
return $this->toBeGreaterThanOrEqual($min)
->toBeLessThanOrEqual($max);
});
// Tests/Unit/ExampleTest.php
test('numeric ranges', function () {
expect(100)->toBeWithinRange(90, 110);
});While users typically utilize Pest's built-in expectations within their custom expectations as demonstrated in the toBeWithinRange() example, there may be times when you need to access the expectation value directly to perform your own custom expectation logic. In such cases, you can access the expectation value that was passed to expect($value) via the $this->value property.
expect()->extend('toBeWithinRange', function (int $min, int $max) {
echo $this->value; // 100
});Of course, you probably want users to have the ability to "chain" expectations together with your custom expectation. To achieve this, ensure your custom expectation includes a return $this statement.
// Pest.php or Expectations.php
expect()->extend('toBeWithinRange', function (int $min, int $max) {
// Assertions based on `$this->value` and the given arguments...
return $this; // Return this, so another expectations can chain this one...
});
// Tests/Unit/ExampleTest.php
test('numeric ranges', function () {
expect(100)
->toBeInt()
->toBeWithinRange(90, 110)
->to...
});Sometimes, you may need to trigger a test failure in your Custom Expectation. To do so, use the test() method in combination with the fail() method.
// Pest.php or Expectations.php
expect()->extend('toBeDivisibleBy', function (int $divisor) {
if ($divisor === 0) {
test()->fail('The divisor cannot be 0.');
}
return expect($this->value % $divisor)->toBe(0);
});
// Tests/Unit/ExampleTest.php
test('numeral division', function () {
expect(10)->toBeDivisibleBy(2); // Pass
expect(10)->toBeDivisibleBy(0); // Fail "The divisor cannot be 0."
});Intercept Expectations
Although it is considered an advanced practice, you can override existing expectations with your own implementation via the intercept() method. When using this method, the existing expectation will be fully substituted if the expectation value is of the specified type. For example, you can replace the toBe() expectation to check if two objects of the Illuminate\Database\Eloquent\Model type have the same id.
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
// tests/Pest.php or tests/Expectations.php
expect()->intercept('toBe', Model::class, function(Model $expected) {
expect($this->value->id)->toBe($expected->id);
});
// tests/Feature/ExampleTest.php
test('models', function () {
$userA = User::find(1);
$userB = User::find(1);
expect($userA)->toBe($userB);
});Instead of passing a string type as the second argument to the intercept() method, you may also pass a closure, which will be invoked to determine whether or not to override the core expectation.
expect()->intercept('toBe', fn (mixed $value) => is_string($value), function (string $expected, bool $ignoreCase = false) {
if ($ignoreCase) {
assertEqualsIgnoringCase($expected, $this->value);
} else {
assertSame($expected, $this->value);
}
});Pipe Expectations
There may be instances where you want to run one of Pest's built-in expectations, but include customized expectation logic under certain conditions. In these cases, you can use the pipe() method. For example, we may want to customize the behavior of the toBe() expectation if the given value is an Eloquent model.
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
expect()->pipe('toBe', function (Closure $next, mixed $expected) {
if ($this->value instanceof Model) {
return expect($this->value->id)->toBe($expected->id);
}
return $next(); // Run to the original, built-in expectation...
});As demonstrated, creating custom expectations can significantly simplify your code by eliminating the need to duplicate the logic to verify that your tests are behaving as anticipated. In the following chapter, we will explore how Pest can scan for Profanity, to help you maintain a professional codebase: Profanity Plugin
File: custom-helpers.md
title: Custom Helpers
Custom Helpers
If you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.
For example, if your helper is specific to a certain test file, you may create the helper in the test file directly. Within your helper, you may invoke the test() function to access the test class instance that would normally be available via $this.
use App\Models\User;
use Tests\TestCase;
function asAdmin(): TestCase
{
$user = User::factory()->create([
'admin' => true,
]);
return test()->actingAs($user);
}
it('can manage users', function () {
asAdmin()->get('/users')->assertOk();
})Note: If your helper creates a custom expectation, you should write a dedicated custom expectation instead.
If your test helpers are utilized throughout your test suite, you may define them within the tests/Pest.php or tests/Helpers.php files. Alternatively, you can create a tests/Helpers directory to house your own helper files. All of these options will be automatically loaded by Pest.
use App\Clients\PaymentClient;
use Mockery;
// tests/Pest.php or tests/Helpers.php
function mockPayments(): object
{
$client = Mockery::mock(PaymentClient::class);
//
return $client;
}
// tests/Feature/PaymentsTest.php
it('may buy a book', function () {
$client = mockPayments();
//
})As an alternative to defining helper methods as functions, you may define protected methods in your base test class and subsequently access them in your test cases using the $this variable.
use App\Clients\PaymentClient;
use PHPUnit\Framework\TestCase as BaseTestCase;
use Mockery;
// tests/TestCase.php
class TestCase extends BaseTestCase
{
protected function mockPayments(): void
{
$client = Mockery::mock(PaymentClient::class);
//
return $client;
}
}
// tests/Pest.php
pest()->extend(TestCase::class)->in('Feature');
// tests/Feature/PaymentsTest.php
it('may buy a book', function () {
$client = $this->mockPayments();
//
})In this section, we explored creating custom helpers. Digging deeper, you may even want to generate a custom expectation. Let's jump into that topic in the next chapter: Custom Expectations
File: datasets.md
title: Datasets
Datasets
With datasets, you can define an array of test data and Pest will run the same test for each set automatically. This saves time and effort by eliminating the need to repeat the same test manually with different data.
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with(['[email protected]', '[email protected]']);When running your tests, Pest will automatically add informative test descriptions to tests that use datasets, outlining the parameters used in each test, aiding in understanding the data and identifying issues if a test fails.
Naturally, it is possible to supply multiple arguments by providing an array containing arrays of arguments.
it('has emails', function (string $name, string $email) {
expect($email)->not->toBeEmpty();
})->with([
['Nuno', '[email protected]'],
['Other', '[email protected]']
]);To manually add your own description to a dataset value, you may simply assign it a key.
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => '[email protected]',
'taylor' => '[email protected]',
]);If a key is added, Pest will use the key when generating the description for the test.
If the test name includes :dataset, the description will be interpolated into the test name at that location.
✓ it validates the "first_name" field
✓ it validates the "email" fieldIt is important to notice that when using closures in your dataset, you must declare the arguments type in the closure passed to the test function.
it('can sum', function (int $a, int $b, int $result) {
expect(sum($a, $b))->toBe($result);
})->with([
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'using closure' => [fn () => 1, 2, 3],
]);For larger or more complex scenarios, closures can be used.
// Returning an array
test('The array contains only integers', function ($i) {
expect($i)->toBeInt();
})->with(fn (): array => range(1, 99));
// Using a generator
test('The generator produces only integers', function ($i) {
expect($i)->toBeInt();
})->with(function (): Generator {
for ($i = 1 ; $i < 100_000_000_000 ; $i++) {
yield $i;
}
});Bound Datasets
Pest's bound datasets can be used to obtain a dataset that is resolved after the beforeEach() method of your tests. This is particularly useful in Laravel applications (or any other Pest integration) where you may need a dataset of App\Models\User models that are created after your database schema is prepared by the beforeEach() method.
it('can generate the full name of a user', function (User $user) {
expect($user->full_name)->toBe("{$user->first_name} {$user->last_name}");
})->with([
fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']),
fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']),
fn() => User::factory()->create(['first_name' => 'Freek', 'last_name' => 'Van Der Herten']),
]);If you want, you can bind a single argument to the test case. However, Pest requires that it must be fully typed in the it|test function arguments.
-it('can generate the full name of a user', function ($user, $fullName) {
+it('can generate the full name of a user', function (User $user, $fullName) {
expect($user->full_name)->toBe($fullName);
})->with([
[fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']), 'Nuno Maduro'],
[fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']), 'Luke Downing'],
[fn() => User::factory()->create(['first_name' => 'Freek', 'last_name' => 'Van Der Herten']), 'Freek Van Der Herten'],
]);Sharing Datasets
By storing your datasets separately in the tests/Datasets folder, you can easily distinguish them from your test code and ensure that they do not clutter your main test files.
// tests/Unit/ExampleTest.php...
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
-})->with(['[email protected]', '[email protected]']);
+})->with('emails');
// tests/Datasets/Emails.php...
+dataset('emails', [
+ '[email protected]',
+ '[email protected]'
+]);Bound datasets, description keys, and other rules that are applicable to inline datasets can also be applied to shared datasets.
Scoped Datasets
Occasionally, datasets may pertain only to a specific feature or set of folders. In such cases, rather than distributing the dataset globally within the Datasets folder, you can generate a Datasets.php file within the relevant folder requiring the dataset and restrict the dataset's scope to that folder alone.
// tests/Feature/Products/ExampleTest.php...
it('has products', function (string $product) {
expect($product)->not->toBeEmpty();
})->with('products');
// tests/Feature/Products/Datasets.php...
dataset('products', [
'egg',
'milk'
]);Combining Datasets
You can easily obtain complex datasets by combining both inline and shared datasets. When doing so, the datasets will be combined using a cartesian product approach.
In the following example, we verify that all of the specified businesses are closed on each of the provided weekdays.
dataset('days_of_the_week', [
'Saturday',
'Sunday',
]);
test('business is closed on day', function(string $business, string $day) {
expect(new $business)->isClosed($day)->toBeTrue();
})->with([
Office::class,
Bank::class,
School::class
])->with('days_of_the_week');When running the example above, Pest's output will contain a description of each of the validated combinations.
Repeating Tests
In some cases, you may need to repeat a test multiple times for debugging purposes or to ensure that the test is stable. On these occasions, you may use the repeat() method to repeat a test a given number of times.
it('can repeat a test', function () {
$result = /** Some code that may be unstable */;
expect($result)->toBeTrue();
})->repeat(100); // Repeat the test 100 timesAfter becoming skilled at utilizing datasets for testing, the next crucial step is to gain an understanding of how to test for exceptions. This involves verifying that your code behaves correctly and throws appropriate exceptions when it encounters unexpected or erroneous input: Exceptions →
File: documentation.md
Press
Getting Started
Digging Deeper
Advanced Topics
More
File: editor-setup.md
title: Editor Setup
Once the installation process is complete, and your editor is ready, you can learn more about how to write tests visiting the next section of the documentation: Writing Tests →
File: exceptions.md
title: Exceptions
Exceptions
When testing behavior in PHP, you might need to check if an exception or error has been thrown. To create a test that expects an exception to be thrown, you can use the throws() method.
it('throws exception', function () {
throw new Exception('Something happened.');
})->throws(Exception::class);If you also want to make an assertion against the exception message, you may provide a second argument to the throws() method.
it('throws exception', function () {
throw new Exception('Something happened.');
})->throws(Exception::class, 'Something happened.');If the exception type is not relevant, and you're only concerned with the message, you can simply pass the message without specifying the exception's type.
it('throws exception', function () {
throw new Exception('Something happened.');
})->throws('Something happened.');You can use the throwsIf() method to conditionally verify an exception if a given boolean expression evaluates to true.
it('throws exception', function () {
//
})->throwsIf(fn() => DB::getDriverName() === 'mysql', Exception::class, 'MySQL is not supported.');Just like throwsIf() method, you can use the throwsUnless() method to conditionally verify an exception if a given boolean expression evaluates to false.
it('throws exception', function () {
//
})->throwsUnless(fn() => DB::getDriverName() === 'mysql', Exception::class, 'Only MySQL is supported.');You can also verify that a given closure throws one or more exceptions using the toThrow() method of the expectation API.
it('throws exception', function () {
expect(fn() => throw new Exception('Something happened.'))->toThrow(Exception::class);
});If you expect no exceptions to be thrown, you can use the throwsNoExceptions() method.
it('throws no exceptions', function () {
$result = 1 + 1;
})->throwsNoExceptions();Sometimes, you may want to simply mark a test as failed. You can use the fail() method to do so.
it('fails', function () {
$this->fail();
});You may also provide a message to the fail() method.
it('fails', function () {
$this->fail('Something went wrong.');
});In addition, you can also use the fails() method to verify if a test fails.
it('fails', function () {
$this->fail('Something happened.');
})->fails();You can also assert the failure reason by providing a message to the fails() method.
it('fails as expected', function () {
$this->fail('Something happened.');
})->fails('Something happened.'); // Pass
it('fails in an unexpected way', function () {
$this->fail('Something unexpected happened.');
})->fails('Something happened.'); // FailAfter learning how to write tests that assert exceptions, the next step is to explore "Test Filtering". This feature allows you to efficiently run specific tests based on criteria like test name, dirty files, and more: Filtering Tests →
File: expectations.md
title: Expectations
Expectations
By setting expectations for your tests using the Pest expectation API, you can easily identify bugs and other issues in your code. This is because the API allows you to specify the expected outcome of a test, making it easy to detect any deviations from the expected behavior.
You can start the expectation by passing your value to the expect($value) function. The expect() function is used every time you want to test a value. You will rarely call expect() by itself. Instead, you will use expect() along with an "expectation" method to assert something about the value.
test('sum', function () {
$value = sum(1, 2);
expect($value)->toBe(3); // Assert that the value is 3...
});As demonstrated, the expect function in Pest allows you to chain multiple expectations together for a given $value. This means that you can perform as many checks as necessary in a single test by simply continuing to chain additional expectations.
expect($value)
->toBeInt()
->toBe(3);At any time, you may test the opposite of an expectation by prepending the not modifier to the expectation.
expect($value)
->toBeInt()
->toBe(3)
->not->toBeString() // Not to be string...
->not->toBe(4); // Not to be 4...With the Pest expectation API, you have access to an extensive collection of individual expectations that are designed to test various aspects of your code. Below is a comprehensive list of the available expectations.
toBe()toBeArray()toBeBetween()toBeEmpty()toBeTrue()toBeTruthy()toBeFalse()toBeFalsy()toBeGreaterThan()toBeGreaterThanOrEqual()toBeLessThan()toBeLessThanOrEqual()toContain()toContainEqual()toContainOnlyInstancesOf()toHaveCount()toHaveProperty()toHaveProperties()toMatchArray()toMatchObject()toEqual()toEqualCanonicalizing()toEqualWithDelta()toBeIn()toBeInfinite()toBeInstanceOf()toBeBool()toBeCallable()toBeFile()toBeFloat()toBeInt()toBeIterable()toBeNumeric()toBeDigits()toBeObject()toBeResource()toBeScalar()toBeString()toBeJson()toBeNan()toBeNull()toHaveKey()toHaveKeys()toHaveLength()toBeReadableDirectory()toBeReadableFile()toBeWritableDirectory()toBeWritableFile()toStartWith()toThrow()toEndWith()toMatch()toMatchConstraint()toBeUppercase()toBeLowercase()toBeAlpha()toBeAlphaNumeric()toBeSnakeCase()toBeKebabCase()toBeCamelCase()toBeStudlyCase()toHaveSnakeCaseKeys()toHaveKebabCaseKeys()toHaveCamelCaseKeys()toHaveStudlyCaseKeys()toHaveSameSize()toBeUrl()toBeUuid()
In addition to the individual expectations available in Pest, the expectation API also provides several modifiers that allow you to further customize your tests. These modifiers can be used to create more complex expectations and to test multiple values at once. Here are some examples of the modifiers available in Pest:
`toBe()`
This expectation ensures that both $value and $expected share the same type and value.
If used with objects, it ensures that both variables refer to the exact same object.
expect(1)->toBe(1);
expect('1')->not->toBe(1);
expect(new StdClass())->not->toBe(new StdClass());`toBeBetween()`
This expectation ensures that $value is between 2 values. It works with int,float and DateTime.
expect(2)->toBeBetween(1, 3);
expect(1.5)->toBeBetween(1, 2);
$expectationDate = new DateTime('2023-09-22');
$oldestDate = new DateTime('2023-09-21');
$latestDate = new DateTime('2023-09-23');
expect($expectationDate)->toBeBetween($oldestDate, $latestDate);`toBeEmpty()`
This expectation ensures that $value is empty.
expect('')->toBeEmpty();
expect([])->toBeEmpty();
expect(null)->toBeEmpty();`toBeTrue()`
This expectation ensures that $value is true.
expect($isPublished)->toBeTrue();`toBeTruthy()`
This expectation ensures that $value is truthy.
expect(1)->toBeTruthy();
expect('1')->toBeTruthy();`toBeFalse()`
This expectation ensures that $value is false.
expect($isPublished)->toBeFalse();`toBeFalsy()`
This expectation ensures that $value is falsy.
expect(0)->toBeFalsy();
expect('')->toBeFalsy();`toBeGreaterThan($expected)`
This expectation ensures that $value is greater than $expected.
expect($count)->toBeGreaterThan(20);`toBeGreaterThanOrEqual($expected)`
This expectation ensures that $value is greater than or equal to $expected.
expect($count)->toBeGreaterThanOrEqual(21);`toBeLessThan($expected)`
This expectation ensures that $value is lesser than $expected.
expect($count)->toBeLessThan(3);`toBeLessThanOrEqual($expected)`
This expectation ensures that $value is lesser than or equal to $expected.
expect($count)->toBeLessThanOrEqual(2);`toContain($needles)`
This expectation ensures that all the given needles are elements of the $value.
expect('Hello World')->toContain('Hello');
expect('Pest: an elegant PHP Testing Framework')->toContain('Pest', 'PHP', 'Framework');
expect([1, 2, 3, 4])->toContain(2, 4);`toContainEqual($needles)`
This expectation ensures that all the given needles are elements (in terms of equality) of the $value.
expect([1, 2, 3])->toContainEqual('1');
expect([1, 2, 3])->toContainEqual('1', '2');`toContainOnlyInstancesOf($class)`
This expectation ensures that $value contains only instances of $class.
$dates = [new DateTime(), new DateTime()];
expect($dates)->toContainOnlyInstancesOf(DateTime::class);`toHaveCount(int $count)`
This expectation ensures that the $count provided matches the number of elements in an iterable $value.
expect(['Nuno', 'Luke', 'Alex', 'Dan'])->toHaveCount(4);`toHaveProperty(string $name, $value = null)`
This expectation ensures that $value has a property named $name.
In addition, you can verify the actual value of a property by providing a second argument.
expect($user)->toHaveProperty('name');
expect($user)->toHaveProperty('name', 'Nuno');
expect($user)->toHaveProperty('is_active', 'true');`toHaveProperties(iterable $name)`
This expectation ensures that $value has property names matching all the names contained in $names.
expect($user)->toHaveProperties(['name', 'email']);In addition, you can verify the name and value of multiple properties using an associative array.
expect($user)->toHaveProperties([
'name' => 'Nuno',
'email' => '[email protected]'
]);`toMatchArray($array)`
This expectation ensures that the $value array matches the given $array subset.
$user = [
'id' => 1,
'name' => 'Nuno',
'email' => '[email protected]',
'is_active' => true,
];
expect($user)->toMatchArray([
'email' => '[email protected]',
'name' => 'Nuno'
]);`toMatchObject($object)`
This expectation ensures that the $value object matches a subset of the properties of a given $object.
$user = new stdClass();
$user->id = 1;
$user->email = '[email protected]';
$user->name = 'Nuno';
expect($user)->toMatchObject([
'email' => '[email protected]',
'name' => 'Nuno'
]);`toEqual($expected)`
This expectation ensures that $value and $expected have the same value.
expect($title)->toEqual('Hello World');
expect('1')->toEqual(1);
expect(new StdClass())->toEqual(new StdClass());`toEqualCanonicalizing($expected)`
This expectation ensures that $value and $expected have the same values, no matter what order the elements are given in.
$usersAsc = ['Dan', 'Fabio', 'Nuno'];
$usersDesc = ['Nuno', 'Fabio', 'Dan'];
expect($usersAsc)->toEqualCanonicalizing($usersDesc);
expect($usersAsc)->not->toEqual($usersDesc);`toEqualWithDelta($expected, float $delta)`
This expectation ensures that the absolute difference between $value and $expected is lower than $delta.
expect($durationInMinutes)->toEqualWithDelta(10, 5); //duration of 10 minutes with 5 minutes tolerance
expect(14)->toEqualWithDelta(10, 5); // Pass
expect(14)->toEqualWithDelta(10, 0.1); // Fail`toBeIn()`
This expectation ensures that $value is one of the given values.
expect($newUser->status)->toBeIn(['pending', 'new', 'active']);`toBeInfinite()`
This expectation ensures that $value is infinite.
expect(log(0))->toBeInfinite();`toBeInstanceOf($class)`
This expectation ensures that $value is an instance of $class.
expect($user)->toBeInstanceOf(User::class);`toBeArray()`
This expectation ensures that $value is an array.
expect(['Pest','PHP','Laravel'])->toBeArray();`toBeBool()`
This expectation ensures that $value is of type bool.
expect($isActive)->toBeBool();`toBeCallable()`
This expectation ensures that $value is of type callable.
$myFunction = function () {};
expect($myFunction)->toBeCallable();`toBeFile()`
This expectation ensures that the string $value is an existing file.
expect('/tmp/some-file.tmp')->toBeFile();`toBeFloat()`
This expectation ensures that $value is of type float.
expect($height)->toBeFloat();`toBeInt()`
This expectation ensures that $value is of type integer.
expect($count)->toBeInt();`toBeIterable()`
This expectation ensures that $value is of type iterable.
expect($array)->toBeIterable();`toBeNumeric()`
This expectation ensures that $value is of type numeric.
expect($age)->toBeNumeric();
expect(10)->toBeNumeric();
expect('10')->toBeNumeric();`toBeDigits()`
This expectation ensures that $value contains only digits.
expect($year)->toBeDigits();
expect(15)->toBeDigits();
expect('15')->toBeDigits();
expect(0.123)->not->toBeDigits();
expect('0.123')->not->toBeDigits();`toBeObject()`
This expectation ensures that $value is of type object.
$object = new stdClass();
expect($object)->toBeObject();`toBeResource()`
This expectation ensures that $value is of type resource.
$handle = fopen('php://memory', 'r+');
expect($handle)->toBeResource();`toBeScalar()`
This expectation ensures that $value is of type scalar.
expect('1')->toBeScalar();
expect(1)->toBeScalar();
expect(1.0)->toBeScalar();
expect(true)->toBeScalar();
expect([1, '1'])->not->toBeScalar();`toBeString()`
This expectation ensures that $value is of type string.
expect($string)->toBeString();`toBeJson()`
This expectation ensures that $value is a JSON string.
expect('{"hello":"world"}')->toBeJson();`toBeNan()`
This expectation ensures that $value is not a number (NaN).
expect(sqrt(-1))->toBeNan();`toBeNull()`
This expectation ensures that $value is null.
expect(null)->toBeNull();`toHaveKey(string $key)`
This expectation ensures that $value contains the provided $key.
expect(['name' => 'Nuno', 'surname' => 'Maduro'])->toHaveKey('name');
expect(['name' => 'Nuno', 'surname' => 'Maduro'])->toHaveKey('name', 'Nuno');
expect(['user' => ['name' => 'Nuno', 'surname' => 'Maduro']])->toHaveKey('user.name');
expect(['user' => ['name' => 'Nuno', 'surname' => 'Maduro']])->toHaveKey('user.name', 'Nuno');`toHaveKeys(array $keys)`
This expectation ensures that $value contains the provided $keys.
expect(['id' => 1, 'name' => 'Nuno'])->toHaveKeys(['id', 'name']);
expect(['message' => ['from' => 'Nuno', 'to' => 'Luke'] ])->toHaveKeys(['message.from', 'message.to']);`toHaveLength(int $number)`
This expectation ensures that the provided $number matches the length of a string $value or the number of elements in an iterable $value.
expect('Pest')->toHaveLength(4);
expect(['Nuno', 'Maduro'])->toHaveLength(2);`toBeDirectory()`
This expectation ensures that the string $value is a directory.
expect('/tmp')->toBeDirectory();`toBeReadableDirectory()`
This expectation ensures that the string $value is a directory and that it is readable.
expect('/tmp')->toBeReadableDirectory();`toBeReadableFile()`
This expectation ensures that the string $value is a file and that it is readable.
expect('/tmp/some-file.tmp')->toBeReadableFile();`toBeWritableDirectory()`
This expectation ensures that the string $value is a directory and that it is writable.
expect('/tmp')->toBeWritableDirectory();`toBeWritableFile()`
This expectation ensures that the string $value is a file and that it is writable.
expect('/tmp/some-file.tmp')->toBeWritableFile();`toStartWith(string $expected)`
This expectation ensures that $value starts with the provided string.
expect('Hello World')->toStartWith('Hello');`toThrow()`
This expectation ensures that a closure throws a specific exception class, exception message, or both.
expect(fn() => throw new Exception('Something happened.'))->toThrow(Exception::class);
expect(fn() => throw new Exception('Something happened.'))->toThrow('Something happened.');
expect(fn() => throw new Exception('Something happened.'))->toThrow(Exception::class, 'Something happened.');
expect(fn() => throw new Exception('Something happened.'))->toThrow(new Exception('Something happened.'));`toMatch(string $expression)`
This expectation ensures that $value matches a regular expression.
expect('Hello World')->toMatch('/^hello wo.*$/i');`toEndWith(string $expected)`
This expectation ensures that $value ends with the provided string.
expect('Hello World')->toEndWith('World');`toMatchConstraint(Constraint $constraint)`
This expectation ensures that $value matches a specified PHPUnit constraint.
use PHPUnit\Framework\Constraint\IsTrue;
expect(true)->toMatchConstraint(new IsTrue());`toBeUppercase(string $expected)`
This expectation ensures that $value is uppercase.
expect('PESTPHP')->toBeUppercase();`toBeLowercase(string $expected)`
This expectation ensures that $value is lowercase.
expect('pestphp')->toBeLowercase();`toBeAlpha(string $expected)`
This expectation ensures that $value only contains alpha characters.
expect('pestphp')->toBeAlpha();`toBeAlphaNumeric(string $expected)`
This expectation ensures that $value only contains alphanumeric characters.
expect('pestPHP123')->toBeAlphaNumeric();`toBeSnakeCase()`
This expectation ensures that $value only contains string in snake_case format.
expect('snake_case')->toBeSnakeCase();`toBeKebabCase()`
This expectation ensures that $value only contains string in kebab-case format.
expect('kebab-case')->toBeKebabCase();`toBeCamelCase()`
This expectation ensures that $value only contains string in camelCase format.
expect('camelCase')->toBeCamelCase();`toBeStudlyCase()`
This expectation ensures that $value only contains string in StudlyCase format.
expect('StudlyCase')->toBeStudlyCase();`toHaveSnakeCaseKeys()`
This expectation ensures that $value only contains an array with keys in snake_case format.
expect(['snake_case' => 'abc123'])->toHaveSnakeCaseKeys();`toHaveKebabCaseKeys()`
This expectation ensures that $value only contains an array with keys in kebab-case format.
expect(['kebab-case' => 'abc123'])->toHaveKebabCaseKeys();`toHaveCamelCaseKeys()`
This expectation ensures that $value only contains an array with keys in camelCase format.
expect(['camelCase' => 'abc123'])->toHaveCamelCaseKeys();`toHaveStudlyCaseKeys()`
This expectation ensures that $value only contains an array with keys in StudlyCase format.
expect(['StudlyCase' => 'abc123'])->toHaveStudlyCaseKeys();`toHaveSameSize()`
This expectation ensures that the size of $value and the provided iterable are the same.
expect(['foo', 'bar'])->toHaveSameSize(['baz', 'bazz']);`toBeUrl()`
This expectation ensures that $value is a URL.
expect('https://pestphp.com/')->toBeUrl();`toBeUuid()`
This expectation ensures that $value is an UUID.
expect('ca0a8228-cdf6-41db-b34b-c2f31485796c')->toBeUuid();`and($value)`
The and() modifier allows you to pass a new $value, enabling you to chain multiple expectations in a single test.
expect($id)->toBe(14)
->and($name)->toBe('Nuno');`dd()`
The dd() modifier
Use the dd() modifier allows you to dump the current expectation $value and stop the code execution. This can be useful for debugging by allowing you to inspect the current state of the $value at a particular point in your test.
expect(14)->dd(); // 14
expect([1, 2])->sequence(
fn ($number) => $number->toBe(1),
fn ($number) => $number->dd(), // 2
);`ddWhen($condition)`
Use the ddWhen() modifier allows you to dump the current expectation $value and stop the code execution when the given $condition is truthy.
expect([1, 2])->each(
fn ($number) => $number->ddWhen(fn (int $number) => $number === 2) // 2
);`ddUnless($condition)`
Use the ddUnless() modifier allows you to dump the current expectation $value and stop the code execution when the given $condition is falsy.
expect([1, 2])->each(
fn ($number) => $number->ddUnless(fn (int $number) => $number === 1) // 2
);`each()`
The each() modifier allows you to create an expectation on each item of the given iterable. It works by iterating over the iterable and applying the expectation to each item.
expect([1, 2, 3])->each->toBeInt();
expect([1, 2, 3])->each->not->toBeString();
expect([1, 2, 3])->each(fn ($number) => $number->toBeLessThan(4));
expect([1, 2, 3])->each(fn ($number, $key) => $number->toEqual($key + 1));`json()`
The json() modifier decodes the current expectation $value from JSON to an array.
expect('{"name":"Nuno","credit":1000.00}')
->json()
->toHaveCount(2)
->name->toBe('Nuno')
->credit->toBeFloat();
expect('not-a-json')->json(); //Fails`match()`
The match() modifier executes the closure associated with the first array key that matches the value of the first argument given to the method.
expect($user->miles)
->match($user->status, [
'new' => fn ($userMiles) => $userMiles->ToBe(0),
'gold' => fn ($userMiles) => $userMiles->toBeGreaterThan(500),
'platinum' => fn ($userMiles) => $userMiles->toBeGreaterThan(1000),
]);To check if the expected value is equal to the value associated with the matching key, you can directly pass the expected value as the array value instead of using a closure.
expect($user->default_language)
->match($user->country, [
'PT' => 'Português',
'US' => 'English',
'TR' => 'Türkçe',
]);`not`
The not modifier allows to invert the subsequent expectation.
expect(10)->not->toBeGreaterThan(100);
expect(true)->not->toBeFalse();`ray()`
The ray() modifier allows you to debug the current $value with myray.app.
expect(14)->ray(); // 14
expect([1, 2])->sequence(
fn ($number) => $number->toBe(1),
fn ($number) => $number->ray(), // 2
);`sequence()`
The sequence() modifier allows you to specify a sequential set of expectations for a single iterable.
expect([1, 2, 3])->sequence(
fn ($number) => $number->toBe(1),
fn ($number) => $number->toBe(2),
fn ($number) => $number->toBe(3),
);The sequence() modifier can also be used with associative iterables. Each closure in the sequence will receive two arguments: the first argument being the expectation for the value and the second argument being the expectation for the key.
expect(['hello' => 'world', 'foo' => 'bar', 'john' => 'doe'])->sequence(
fn ($value, $key) => $value->toEqual('world'),
fn ($value, $key) => $key->toEqual('foo'),
fn ($value, $key) => $value->toBeString(),
);The sequence() modifier can also be used to check if each value in the iterable matches a set of expected values. In this case, you can pass the expected values directly to the sequence() method instead of using closures.
expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz');`when()`
The when() modifier runs the provided callback when the first argument passed to the method evaluates to true.
expect($user)
->when($user->is_verified === true, fn ($user) => $user->daily_limit->toBeGreaterThan(10))
->email->not->toBeEmpty();`unless()`
The unless() modifier runs the provided callback when the first argument passed to the method evaluates to false.
expect($user)
->unless($user->is_verified === true, fn ($user) => $user->daily_limit->toBe(10))
->email->not->toBeEmpty();After learning how to write expectations, the next section in the documentation, "Hooks" covers useful functions like "beforeEach" and "afterEach" that can be used to set up preconditions and cleanup actions for your tests: Hooks →
File: filtering-tests.md
title: Filtering Tests
Test Filtering
When you run ./vendor/bin/pest, Pest executes the complete test suite by default. As you might expect, running individual tests is accomplished by passing the test name as the first argument.
./vendor/bin/pest tests/Unit/TestExample.phpThis chapter will cover even more ways to filter which tests are executed by Pest. For the complete CLI API Reference, please refer to our CLI API Reference.
`--bail`
The --bail option instructs Pest to stop executing your test suite upon encountering the first failure or error.
./vendor/bin/pest --bail`--dirty`
The --dirty option instructs Pest to only run tests that have uncommitted changes according to Git. This is often useful when you're developing a set of tests for a new feature and don't want to run the entire test suite each time Pest is invoked.
./vendor/bin/pest --dirtyNote that, due to a limitation in Pest, test cases written using the PHPUnit syntax will always be considered dirty.
`--filter`
Using the --filter option, it is possible to run tests that match a specified regular expression pattern. The --filter option allows you to filter tests based on any information that would typically appear in the test's output description, such as the filename, test description, dataset parameters, and more.
./vendor/bin/pest --filter "test description"`--group`
You can utilize the --group option to selectively run tests belonging to a particular group. To learn about assigning a tests or folders to groups, please refer to the Grouping Tests documentation.
./vendor/bin/pest --group=integration,browser`--exclude-group`
The --exclude-group option may be used to exclude specific test groups from being executed.
./vendor/bin/pest --exclude-group=integration,browser`--retry`
If a test previously failed, you typically want to sort the failed tests by arranging the test suite to run them first. In such cases, you can use the --retry option.
The --retry flag reorders your test suites by prioritizing the previously failed tests. If there were no past failures, the suite runs as usual. But if there were previous failures, those tests are run first.
Note: Keep in mind that if your
phpunit.xmlfile has two test suites (usually Unit and Feature), this option will sort each suite by running the failed tests first. This means that sometimes, you may see the entire Unit test suite run before Pest runs the Feature test suite, where previously failed tests take priority.
./vendor/bin/pest --retry`only()`
If you want to run a specific test in your test suite, you can use the only() method.
test('sum', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
})->only();As your codebase grows, manually running your tests with filtering can become tedious. That's where skipping tests comes in. Skipping tests is a useful feature that allows developers to exclude specific tests from the test suite temporarily without deleting them entirely: Skipping Tests →
File: global-hooks.md
title: Global Hooks
Global Hooks
As previously discussed, hooks allow you to simplify your testing process and automate repetitive tasks that you may perform before or after a test. However, if the hooks are the same across multiple test files, you may wish to define "global" hooks to avoid code duplication. Global hooks are defined in your Pest.php configuration file.
For instance, if you need to perform some database operations before each test within the Feature folder, you may use the beforeEach() hook within your Pest.php configuration file.
pest()->extend(TestCase::class)->beforeEach(function () {
// Interact with your database...
})->group('integration')->in('Feature');In addition, you can define global hooks that will run before or after your entire test suite, regardless of the folder or group.
pest()->beforeEach(function () {
// Interact with your database...
});In fact, any of the hooks mentioned in the hooks documentation can also be used in your Pest.php configuration file.
pest()->extend(TestCase::class)->beforeAll(function () {
// Runs before each file...
})->beforeEach(function () {
// Runs before each test...
})->afterEach(function () {
// Runs after each test...
})->afterAll(function () {
// Runs after each file...
})->group('integration')->in('Feature');Any before* hooks defined in the Pest.php configuration file will be executed prior to hooks defined in individual test files. Similarly, any after* hooks specified in the Pest.php configuration file will be executed after any hooks defined in individual test files.
When setting up a test suite, it may be necessary to mock certain functionality or objects in order to isolate the code being tested and to simulate certain conditions or behaviors. This can be done through the use of mocking libraries or frameworks, such as Mockery: Mocking
File: grouping-tests.md
title: Grouping Tests
Grouping Tests
You can assign tests folders to various groups using Pest's group() method. Assigning a group to a set of relatively slow tests could be beneficial since it allows you to selectively execute them separately from the rest of your test suite. Typically, the process of assigning a set of tests to a group is done within your Pest.php configuration file.
For instance, consider the scenario where we assign the tests located in the tests/Feature folder to a group named "feature".
pest()->extend(TestCase::class)
->group('feature')
->in('Feature');As previously stated in the Filtering Tests documentation, you can use the --group option to execute tests belonging to a specific group.
./vendor/bin/pest --group=featureYou also have the option to assign a particular test to a group by chaining the group() method onto the test function.
it('has home', function () {
//
})->group('feature');You may also assign a test to multiple groups.
it('has home', function () {
//
})->group('feature', 'browser');If you want to assign a group to a describe block, you can do so by chaining the group() method onto the describe function.
describe('home', function () {
test('main page', function () {
//
});
})->group('feature');In some cases, you may want to assign a whole file to a group. To do so, you may use the pest()->group() method within the file.
pest()->group('feature');
it('has home', function () {
//
});When you are setting up a test suite, it may be necessary to share common hooks between different folders and groups. In such cases, Global Hooks can prove to be helpful: Global Hooks
File: higher-order-testing.md
title: Higher Order Testing
Higher Order Testing
Although "High Order Testing" may appear to be a complex term, it is actually a technique that simplifies your tests and it is entirely optional. One of the core philosophies of Pest is to encourage users to care about the beauty and simplicity of their test suite, just as they do about their source code. Therefore, you might find this technique intriguing and choose to adopt it in certain parts of your code.
Let's consider an example that demonstrates how to migrate an existing test to high order testing. To illustrate, we will use a simple test.
it('works', function () {
$this->get('/')
->assertStatus(200);
});Based on this example, you can see that the entire content of the test is chained calls made on the $this variable. In such cases, it is possible to eliminate the test closure entirely and chain the required methods together directly to the it() function.
it('works')
->get('/')
->assertStatus(200);The technique of removing the closure function and directly chaining the methods of the test body to the test() or it() functions is commonly referred to as "High Order Testing". This approach can significantly simplify the code of your test suite.
This technique can also be combined with the expectation API. Let's look at a test where the expectation API is used to verify that a user was created with the correct name.
it('has a name', function () {
$user = User::create([
'name' => 'Nuno Maduro',
]);
expect($user->name)->toBe('Nuno Maduro');
});If your test contains only one expectation, we can simplify it using high-order testing.
it('has a name')
->expect(fn () => User::create(['name' => 'Nuno Maduro'])->name)
->toBe('Nuno Maduro');It is crucial to use lazy evaluation for the expectation value by passing a closure to the expect() method. This ensures that the expected value is created only when the test runs and not before.
If you need to make assertions on an object that requires lazy evaluation at runtime, you can use the defer() method.
it('creates admins')
->defer(fn () => $this->artisan('user:create --admin'))
->assertDatabaseHas('users', ['id' => 1]);In the example above, the assertDatabaseHas() assertion method will be called on the result of the closure passed to the defer() method.
The principles of high-order testing can also be applied to hooks. This means that if the body of your hook consists of a sequence of methods chained to the $this variable, you can simply chain those methods to the hook method and omit the closure entirely.
beforeEach(function () {
$this->withoutMiddleware();
});
// Can be rewritten as...
beforeEach()->withoutMiddleware();When using higher order testing, dataset values are passed to the expect() and defer() closures for convenience.
it('validates emails')
->with(['[email protected]', '[email protected]'])
->expect(fn (string $email) => Validator::isValid($email))
->toBeTrue();Higher Order Expectations
With Higher Order Expectations, you can perform expectations directly on the properties or methods of the expectation $value.
For example, imagine you're testing that a user was created successfully and a variety of attributes have been stored in the database. Your test might look something like this:
expect($user->name)->toBe('Nuno');
expect($user->surname)->toBe('Maduro');
expect($user->addTitle('Mr.'))->toBe('Mr. Nuno Maduro');To utilize Higher Order Expectations, you can simply chain the properties and methods directly to the expect() function, and Pest will take care of retrieving the property value or calling the method on the $value under test.
Now, let's see the same test refactored to Higher Order Expectations.
expect($user)
->name->toBe('Nuno')
->surname->toBe('Maduro')
->addTitle('Mr.')->toBe('Mr. Nuno Maduro');When working with arrays, you may also access the $value array keys and perform expectations on them.
expect(['name' => 'Nuno', 'projects' => ['Pest', 'OpenAI', 'Laravel Zero']])
->name->toBe('Nuno')
->projects->toHaveCount(3)
->each->toBeString();
expect(['Dan', 'Luke', 'Nuno'])
->{0}->toBe('Dan');Higher Order Expectations can be used with all Expectations, and you may even create further Higher Order Expectations within closures.
expect(['name' => 'Nuno', 'projects' => ['Pest', 'OpenAI', 'Laravel Zero']])
->name->toBe('Nuno')
->projects->toHaveCount(3)
->sequence(
fn ($project) => $project->toBe('Pest'),
fn ($project) => $project->toBe('OpenAI'),
fn ($project) => $project->toBe('Laravel Zero'),
);Scoped Higher Order Expectations
With Scoped Higher Order Expectations, you may use the method scoped() and a closure to gain access and lock an expectation in to a certain level in the chain.
This is very useful for Laravel Eloquent models, where you want to check properties of a child relation.
expect($user)
->name->toBe('Nuno')
->email->toBe('[email protected]')
->address()->scoped(fn ($address) => $address
->line1->toBe('1 Pest Street')
->city->toBe('Lisbon')
->country->toBe('Portugal')
);Although higher order testing may appear complicated, it is a technique that can significantly simplify your test suite's code. In the next section, we will discuss Pest's community video resources: Video Resources
File: hooks.md
title: Hooks
Hooks
Pest hooks are similar to the steps that you might take when preparing a meal - first, you gather and prepare the ingredients, then you cook the meal, and finally, you clean up after yourself. In the same way, hooks allow you to perform specific actions before and after each test or file, such as setting up test data, initializing the test environment, or cleaning up resources after the tests are complete.
By using hooks in Pest, you can streamline your testing process, automate repetitive tasks. Whether you're writing unit tests for a small project or building a complex test suite for a large application, hooks can help you save time and improve the quality of your tests.
In addition, if you wish to run a hook only for a specific group of tests, you may include the hook within a describe() function.
beforeEach(function () {
//
});
describe('something', function () {
beforeEach(function () {
//
});
//
describe('something else', function () {
beforeEach(function () {
//
});
//
});
});
test('something', function () {
//
});Here's a list of the hooks that are available in Pest:
`beforeEach()`
Executes the provided closure before every test within the current file, ensuring that any necessary setup or configuration is completed before each test.
beforeEach(function () {
// Prepare something before each test run...
});When using the beforeEach() hook, it's possible to initialize properties that will be shared across all tests within the current file. For example, you could use beforeEach() to initialize the $repository property before each test is run, ensuring that it's available for subsequent tests in the file.
beforeEach(function () {
$this->userRepository = new UserRepository();
});
it('may be created', function () {
$user = $this->userRepository->create();
expect($user)->toBeInstanceOf(User::class);
});`afterEach()`
Executes the provided closure after every test within the current file, allowing you to clean up any resources or state that may have been modified during testing.
afterEach(function () {
// Clear testing data after each test run...
});So, using the example above, if the beforeEach() hook is used to initialize the $userRepository property, the afterEach() hook may be used to "clean" it after each test if necessary. This ensures that any resources the object may be using are released or reset between tests, preventing any interference or unwanted behavior.
afterEach(function () {
$this->userRepository->reset();
});Optionally, you can use the after() method to perform clean-up tasks after a specific test. This is useful when you need to clean up resources that are specific to a single test, rather than shared across all tests in the file.
it('may be created', function () {
$this->userRepository->create();
expect($user)->toBeInstanceOf(User::class);
})->after(function () {
$this->userRepository->reset();
});`beforeAll()`
Executes the provided closure once before any tests are run within the current file, allowing you to perform any necessary setup or initialization that applies to all tests.
beforeAll(function () {
// Prepare something once before any of this file's tests run...
});It's important to note that unlike the beforeEach() hook, the $this variable is not available in the beforeAll() hook. This is because the hook runs before any tests are executed, so there is no instance of the test class or object to which the variable could refer.
`afterAll()`
Executes the provided closure once after all tests have completed within the current file, allowing you to perform any necessary clean-up or tear-down tasks.
afterAll(function () {
// Clean testing data after all tests run...
});Just like the beforeAll() method, the $this variable is not available in the afterAll() hook. This is because the afterAll() hook typically runs after all tests in the file have completed, so there is no longer a test instance or object to which the variable could refer.
Once you've mastered using hooks to set up preconditions and clean-up actions for your tests, we're ready to discuss "Datasets", which allow you to run the same test with different inputs or parameters. Datasets can be used to thoroughly test your code under a variety of conditions and edge cases, letting you identify and fix bugs that may not be immediately obvious: Datasets →
File: installation.md
title: Installation
Installation
Requirements: PHP 8.3+
Installing Pest PHP Testing Framework is a simple process that can be completed in just a few steps. Before you begin, make sure you have PHP 8.3+ or higher installed on your system.
The first step is to require Pest as a "dev" dependency in your project by running the following commands on your command line.
composer remove phpunit/phpunit
composer require pestphp/pest --dev --with-all-dependenciesSecondly, you'll need to initialize Pest in your current PHP project. This step will create a configuration file named Pest.php at the root level of your test suite, which will enable you to fine-tune your test suite later.
./vendor/bin/pest --initFinally, you can run your tests by executing the pest command.
./vendor/bin/pestHere is an example of the output displayed when running Pest in a new, fresh project.
If you're planning on using Browser Testing, you can install the pest-plugin-browser package to get started with browser testing in Pest. For more information, check out the Browser Testing documentation.
Optionally, if you are migrating from PHPUnit, you can use the pest-plugin-drift package to automatically convert your PHPUnit tests to Pest. For more information, check out the Migrating from PHPUnit guide.
After the installation process is finished, you can enhance your developer experience while working with Pest by configuring your editor: Editor Setup →. If you're migrating from PHPUnit, check out the Migration Guide →.
File: LICENSE.md
The MIT License (MIT)
Copyright (c) Nuno Maduro [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File: migrating-from-phpunit-guide.md
title: Migrating from PHPUnit
Migrating from PHPUnit
Pest is built on top of PHPUnit, so migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps. Once you have Pest installed, you should require the pestphp/pest-plugin-drift package as a "dev" dependency in your project.
composer require pestphp/pest-plugin-drift --devDrift is a simple yet powerful plugin that will automatically convert your PHPUnit tests to Pest, simply by running the --drift option.
./vendor/bin/pest --driftSo, typically, a PHPUnit test looks like this:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}Should look like this after running --drift:
test('true is true', function () {
expect(true)->toBeTrue();
});Convert Phpunit tests only for certain folder
If you want to convert Phpunit tests only for certain folder, you can pass path as first argument when calling --drift:
Example call, if you want to run the conversion for folder /tests/Helpers:
/vendor/bin/pest --drift tests/HelpersOutput:
/vendor/bin/pest --drift tests/Helpers
✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔
INFO The [tests/Helpers] directory has been migrated to PEST with XY files changed.The output will contain a summary of the conversion process, as well as a list of the files that were converted.
While most of your tests should be converted automatically, and you should be able to run them without any issues, there are some cases where you may need to manually convert some of your tests.
Of course, this particular chapter is only for those who are migrating from PHPUnit. Next, let's learn how you can contribute to the growth of Pest: Community Guide
File: mocking.md
title: Mocking
Mocking
Requirements: Mockery 1.0+
When testing your applications, you may want to "mock" specific classes to prevent them from actually being invoked during a particular test. For instance, if your application interacts with an API that initiates a payment, you likely want to "mock" the API client locally to prevent the actual payment from being made.
Before getting started, you will need to install a mocking library. We recommend Mockery, but you are free to choose any other library that suits your needs.
To begin using Mockery, require it using the Composer package manager.
composer require mockery/mockery --devWhile comprehensive documentation for Mockery can be found on the Mockery website, this section will discuss the most common use cases for mocking.
Method Expectations
Mock objects are essential for isolating the code being tested and simulating specific behaviors or conditions from other pieces of the application. After creating a mock using the Mockery::mock() method, we can indicate that we expect a certain method to be invoked by calling the shouldReceive() method.
use App\Repositories\BookRepository;
use Mockery;
it('may buy a book', function () {
$client = Mockery::mock(PaymentClient::class);
$client->shouldReceive('post');
$books = new BookRepository($client);
$books->buy(); // The API is not actually invoked since `$client->post()` has been mocked...
});It is possible to mock multiple method calls using the same syntax shown above.
$client->shouldReceive('post');
$client->shouldReceive('delete');Argument Expectations
In order to make our expectations for a method more specific, we can use constraints to limit the expected argument list for a method call. This can be done by utilizing the with() method, as demonstrated in the following example.
$client->shouldReceive('post')
->with($firstArgument, $secondArgument);In order to increase the flexibility of argument matching, Mockery provides built-in matcher classes that can be used in place of specific values. For example, instead of using specific values, we can use Mockery::any() to match any argument.
$client->shouldReceive('post')
->with($firstArgument, Mockery::any());It is important to note that expectations defined using shouldReceive() and with() only apply when the method is invoked with the exact arguments that you expected. Otherwise, Mockery will throw an exception.
$client->shouldReceive('post')->with(1);
$client->post(2); // fails, throws a `NoMatchingExpectationException`In certain cases, it may be more appropriate to use a closure to match all passed arguments simultaneously, rather than relying on built-in matchers for each individual argument. The withArgs() method accepts a closure that receives all of the arguments passed to the expected method call. As a result, this expectation will only be applied to method calls in which the passed arguments cause the closure to evaluate to true.
$client->shouldReceive('post')->withArgs(function ($arg) {
return $arg === 1;
});
$client->post(1); // passes, matches the expectation
$client->post(2); // fails, throws a `NoMatchingExpectationException`Return Values
When working with mock objects, we can use the andReturn() method to tell Mockery what to return from the mocked methods.
$client->shouldReceive('post')->andReturn('post response');We can define a sequence of return values by passing multiple return values to the andReturn() method.
$client->shouldReceive('post')->andReturn(1, 2);
$client->post(); // int(1)
$client->post(); // int(2)Sometimes, we may need to calculate the return results of method calls based on the arguments passed to the method. This can be accomplished using the andReturnUsing() method, which accepts one or more closures.
$mock->shouldReceive('post')
->andReturnUsing(
fn () => 1,
fn () => 2,
);In addition, we can instruct mocked methods to throw exceptions.
$client->shouldReceive('post')->andThrow(new Exception);Method Call "Count" Expectations
Along with specifying expected arguments and return values for method calls, we can also set expectations for how many times a particular method should be invoked.
$mock->shouldReceive('post')->once();
$mock->shouldReceive('put')->twice();
$mock->shouldReceive('delete')->times(3);
// ...To specify a minimum number of times a method should be called, we may use the atLeast() method.
$mock->shouldReceive('delete')->atLeast()->times(3);Mockery's atMost() method allows us to specify the maximum number of times a method can be called.
$mock->shouldReceive('delete')->atMost()->times(3);The primary objective of this section is to provide you with an introduction to Mockery, the mocking library we prefer. However, for a more comprehensive understanding of Mockery, we suggest checking out its official documentation. Next, let's explore Pest's plugins and discover how they can enhance your Pest experience: Plugins
File: mutation-testing.md
title: Mutation Testing
Mutation Testing
Get Started
Requires XDebug 3.0+ or PCOV.
Mutation Testing is an innovative new technique that introduces small changes (mutations) to your code to see if your tests catch them. This ensures you’re testing your application thoroughly, beyond just achieving code coverage and more about the actual quality of the tests. It’s a great way to identify weaknesses in your test suite and improve quality.
To get started with mutation testing, head over to your test file, and be specific about which part of your code your test covers using the covers() function or the mutates function.
covers(TodoController::class); // or mutates(TodoController::class);
it('list todos', function () {
$this->getJson('/todos')->assertStatus(200);
});Both the covers and mutates functions are identical when it comes to mutation testing. However, covers also affects the code coverage report. If provided, it filters the code coverage report to include only the executed code from the referenced code parts.
Then, run Pest PHP with the --mutate option to start mutation testing. Ideally, using the --parallel option to speed up the process.
./vendor/bin/pest --mutate
# or in parallel...
./vendor/bin/pest --mutate --parallelPest will then re-run your tests against "mutated" code and see if the tests are still passing. If a test is still passing against a mutation, it means that the test is not covering that specific part of the code. As, as result, Pest will output the mutation and the diff of the code.
UNTESTED app/Http/TodoController.php > Line 44: ReturnValue - ID: 76d17ad63bb7c307
class TodoController {
public function index(): array
{
// pest detected that this code is untested because
// the test is not covering the return value
- return Todo::all()->toArray();
+ return [];
}
}
Mutations: 1 untested
Score: 33.44%Once you have identified the untested code, you can write additional tests to cover it.
covers(TodoController::class);
it('list todos', function () {
+ Todo::factory()->create(['name' => 'Buy milk']);
- $this->getJson('/todos')->assertStatus(200);
+ $this->getJson('/todos')->assertStatus(200)->assertJson([['name' => 'Buy milk']]);
});Then, you can re-run Pest with the --mutate option to see if the mutation is now "tested" and covered.
Mutations: 1 tested
Score: 100.00%The higher the mutation score, the better your test suite is. A mutation score of 100% means that all mutations were "tested", which is the goal of mutation testing.
Now, if you see "untested" or "uncovered" mutations, or are a mutation score below 100%, typically means that you have missing tests or that your tests are not covering all the edge cases.
Our plugin is deeply integrated into Pest PHP. So, each time a mutation is introduced, Pest PHP will:
- Only run the tests covering the mutated code to speed up the process.
- Cache as much as possible to speed up the process on subsequent runs.
- If enabled, use parallel execution to run multiple tests in parallel to speed up the process.
Tested Vs Untested Mutations
When running mutation testing, you will "mainly" see two types of mutations: tested and untested mutations.
- Tested Mutations: These are mutations that were detected by your test suite. They are considered "tested" because your tests were able to catch the changes introduced by the mutation.
As example, the following mutation is considered "tested" because the test suite was able to detect the change.
class TodoController
{
public function index(): array
{
- return Todo::all()->toArray();
+ return [];
}
}
it('list todos', function () {
Todo::factory()->create(['name' => 'Buy milk']);
// this fails because the mutation changed the return value, proving that the test is working and testing the return value...
$this->getJson('/todos')->assertStatus(200)->assertJsonContains([
['name' => 'Buy milk'],
]);
});- Untested Mutations: These are mutations that were not detected by your test suite. They are considered "untested" because your tests were not able to catch the changes introduced by the mutation.
As example, the following mutation is considered "untested" because the test suite was not able to detect the change.
class TodoController
{
public function index(): array
{
- return Todo::all()->toArray();
+ return [];
}
}
it('list todos', function () {
Todo::factory()->create(['name' => 'Buy milk']);
// this test still passes even though the return value was changed by the mutation...
$this->getJson('/todos')->assertStatus(200);
});Changing the return value is only one of many possible mutations. Typically, a mutation can be a change in the return value, a change in the method call, a change in the method arguments, and so on.
Minimum Threshold Enforcement
To ensure comprehensive testing and maintain testing quality, it is crucial to set minimum threshold values for mutation testing results. In Pest, you can use the --mutation and --min options to define the minimum threshold values for mutation testing score results. If the specified thresholds are not met, Pest will report a failure.
./vendor/bin/pest --mutate --min=40Options & Modifiers
The following options and modifiers are available when running mutation testing.
@pest-mutate-ignore--id--everything--covered-only--bail--class--ignore--clear-cache--no-cache--ignore-min-score-on-zero-mutations--profile--retry--stop-on-uncovered--stop-on-untested
`@pest-mutate-ignore`
Ignore the given line of code when generating mutations.
public function rules(): array
{
return [
'name' => 'required',
'email' => 'required|email', // @pest-mutate-ignore
];
}[!NOTE]
Lines that aren't considered executable will always be marked as UNCOVERED.
For such cases, like with model properties, you may apply @pest-mutate-ignore in the following way.
/**
* @pest-mutate-ignore
*/
protected $guarded = [
'id',
'created_at',
'updated_at',
];
/**
* @pest-mutate-ignore
*/
protected $hidden = [
'id',
'created_at',
'updated_at',
];`--id`
Run only the mutation with the given ID. Note, you need to provide the same options as the original run.
./vendor/bin/pest --mutate --id=ecb35ab30ffd3491`--everything`
Generate mutations for all your project's classes, bypassing the covers() method. This option is very resource-intensive and should be used combined with the --covered-only option.
./vendor/bin/pest --mutate --everything --parallel --covered-onlyIdeally, you would also combine the --parallel option to speed up the process.
`--covered-only`
Only generate mutations in the lines of code that are covered by tests.
./vendor/bin/pest --mutate --covered-only`--bail`
Stop mutation testing execution upon the first untested or uncovered mutation.
./vendor/bin/pest --mutate --bail`--class`
Generate mutations for the given class(es). E.g. --class=App\Models.
./vendor/bin/pest --mutate --class=App\Models`--ignore`
Ignore the given class(es) when generating mutations. E.g. --ignore=App\Http\Requests.
./vendor/bin/pest --mutate --ignore=App\Http\Requests`--clear-cache`
Clears the mutation cache and runs mutation testing from scratch.
./vendor/bin/pest --mutate --clear-cache`--no-cache`
Runs mutation testing without using cached mutations.
./vendor/bin/pest --mutate --no-cache`--ignore-min-score-on-zero-mutations`
Ignore the minimum score requirement when there are no mutations.
./vendor/bin/pest --mutate --min=80 --ignore-min-score-on-zero-mutations`--profile`
Output to standard output the top ten slowest mutations.
./vendor/bin/pest --mutate --profile`--retry`
Run untested or uncovered mutations first and stop execution upon the first error or failure.
./vendor/bin/pest --mutate --retry`--stop-on-uncovered`
Stop mutation testing execution upon the first untested mutation.
./vendor/bin/pest --mutate --stop-on-uncovered`--stop-on-untested`
Stop mutation testing execution upon the first untested mutation.
./vendor/bin/pest --mutate --stop-on-untestedAs you can see Pest PHP's mutation testing feature is a powerful tool to improve the quality of your test suite. In the following chapter, we explain how can you use Snapshots to test your code: Snapshot Testing
File: optimizing-tests.md
title: Optimizing Tests
Optimizing Tests
Pest offers several optimization techniques to help developers write efficient and high-performing tests. One of the most important is parallel testing, which allows multiple tests to run simultaneously across multiple processes using the --parallel option. This can greatly reduce the time it takes to run tests and improve the overall performance of your test suite.
Additionally, Pest provides the --profile flag to quickly identify slow-running tests, allowing you to optimize their execution.
Finally, it's often useful to focus solely on your test suite's failures. To do this, you can utilize the --compact printer when running Pest. This instructs Pest to only display information regarding your test suite's failing tests.
Parallel Testing
By default, Pest executes your tests sequentially within a single process. However, you can significantly decrease the time needed to run your tests by using the --parallel option to run them concurrently across multiple processes. If you’re running on Windows, be sure to use a WSL terminal.
./vendor/bin/pest --parallelWhen running tests in parallel, Pest will create a process for each available CPU core on your machine. However, you can manually modify the number of processes using the --processes option.
./vendor/bin/pest --parallel --processes=10Here are some important points to consider when writing tests that will be executed in parallel:
- Database resources may not be shared between tests: Each test should be isolated and independent from other tests.
- Test order may not be guaranteed: Tests should not rely on any specific order of execution.
- Tests may be affected by race conditions: Race conditions can occur when multiple processes or threads are accessing shared resources. Make sure to design your tests to handle potential race conditions and avoid them whenever possible.
Profiling
Imagine you have a large test suite that takes several minutes to run. You've noticed that some tests take significantly longer than others, but you're not sure which tests are the slowest or what's causing the slowdown.
To identify the slowest tests and optimize their execution, you can use Pest's --profile option. When you run your test suite with this flag enabled, Pest will collect the duration of each test and provide a report that highlights the slowest tests.
./vendor/bin/pest --profileFor example, imagine you run your test suite and see the following output:
You can see that the UserTest > create user and OrderTest > create order tests are taking significantly longer than the other tests. By analyzing this test, you may discover that it's executing several inefficient database queries or performing other expensive operations that could be optimized to reduce its execution time.
Compact Printer
If you're working with a large number of tests, it can be beneficial to concentrate solely on the failing tests. You can use the --compact printer to instruct Pest to only display test failures, making it easier to pinpoint and resolve any problems without the noise of all of your successful tests.
Furthermore, since the --compact printer produces simpler output, test speed can be improved by a few milliseconds since there is less input / output required for each test.
You may even configure Pest to always use the compact printer so you don't have to specify the --compact option every time you run your test suite.
// tests/Pest.php
pest()->printer()->compact();
//Now that you have learned how to speed up your test suite, let's move on to discussing Continuous Integration: Continuous Integration
File: pest-spicy-summer-release.md
title: Pest's Spicy Summer Release
Pest's Spicy Summer Release
"Spicy Summer" is the codename assigned to Pest 2.9.
On March 20, 2023, we proudly introduced Pest 2.0, marking it as our most significant release to date, with more than 7 million downloads at the time of writing. This version showcased a remarkable architectural plugin, an 80% speed improvement in parallel testing, profiling options, and numerous other features.
As we approach summer, we are thrilled to announce our upcoming release: the highly anticipated "Spicy Summer" release. This release brings an array of exciting features that will make it feel like a major version without actually being one - it's Pest v2.9.0 - so it's just a "composer update" away from you. Without further delay, let's dive into what we have in store for you this summer:
- Built-in Snapshot Testing, for testing the long-output of your code with ease
- Describe Blocks, for grouping tests and sharing setup and teardown logic
- Architectural Testing++, even more powerful architectural testing
- Type Coverage Plugin, for measuring the percentage of code that is covered by type declarations
- Drift Plugin, to automatically convert your PHPUnit tests to Pest
Built-in Snapshot Testing
Read the full documentation at: pestphp.com/docs/snapshot-testing
Snapshot testing is a testing technique that allows you to assert that the output of a function or method has not changed. It's a great way to test your codebase and ensure that your code is not changing unexpectedly.
And now, we are proud to announce that Pest will have built-in snapshot testing support. As an example, let's say your "contacts" endpoint outputs a certain HTML every time it runs. You would probably write a test like this:
it('has a contact page', function () {
$response = $this->get('/contact');
expect($response)->toMatchSnapshot();
});The first time you run this test, it will create a snapshot file - at tests/.pest/snapshots - with the response content. The next time you run the test, it will compare the response with the snapshot file. If the response is different, the test will fail. If the response is the same, the test will pass.
In addition, the given expectation value doesn't have to be a response; it can be anything. For example, you can snapshot an array:
$array = /** Fetch array somewhere */;
expect($array)->toMatchSnapshot();And of course, you can "rebuild" the snapshots at any time by using the --update-snapshots option:
./vendor/bin/pest --update-snapshotsDescribe Blocks
Since we released Pest, describe blocks has been one of the most requested features. These are fundamental to any "functional" testing framework, as they allow you to group tests and share setup and teardown logic.
beforeEach(fn () => $this->user = User::factory()->create());
describe('auth', function () {
beforeEach(fn () => $this->actingAs($this->user));
test('cannot login when already logged in', function () {
// ...
});
test('can logout', function () {
// ...
});
})->skip(/* Skip the entire describe block */);
describe('guest', function () {
test('can login', function () {
// ...
});
// ...
});Architectural Testing++
Read the full documentation at: pestphp.com/docs/arch-testing
Pest has always been about making testing more enjoyable. Last release, we introduced architectural expectations, which allow you to test your codebase's architecture. This release, we are proud to announce that Pest improves architectural expectations by adding new ones.
test('controllers')
->expect('App\Http\Controllers')
->toUseStrictTypes()
->toHaveSuffix('Controller') // or toHavePreffix, ...
->toBeReadonly()
->toBeClasses() // or toBeInterfaces, toBeTraits, ...
->classes->not->toBeFinal() // 🌶
->classes->toExtendNothing() // or toExtend(Controller::class),
->classes->toImplementNothing() // or toImplement(ShouldQueue::class),Type Coverage Plugin
Read the full documentation at: pestphp.com/docs/type-coverage
As you may know, Pest offers a --coverage flag that allows you to generate a gorgeous coverage report on the terminal. This report shows you which lines of code are covered by your tests. This is a great way to ensure that your tests are covering all of your code.
To add to this, we are proud to announce that Pest will now have built-in type coverage support. This means that you can now see if your source code is using "types" in every possible place. For example, let's say you have a repository that has the following method:
public function find($id)
{
return User::find($id);
}This method is missing a parameter type and a return type. So, if you run pest --type-coverage, you will see the following output and know that you need to add types to this method:
...
app/Models\User.php .......................................... 100%
app/Repositories/UserRepository.php .................. pa8, rt8 33%
───────────────────────────────────────────────────────────────────
Total: 91.6 %In addition, just like regular coverage, you may enforce --min type coverage percentage. For example, if you run --type-coverage --min=100, you will see the following output:
...
app/Models\User.php .......................................................... 100%
app/Repositories/UserRepository.php .................................. pa8, rt8 33%
───────────────────────────────────────────────────────────────────────────────────
Total: 91.6 %
ERROR Type coverage below expected: 91.6%. Minimum: 100.0%Drift Plugin
Read the full documentation at: pestphp.com/docs/migrating-from-phpunit-guide
Yes, you read that right. We are proud to announce that Pest will now have a Laravel shift-like tool called Drift. Drift will allow you to upgrade your PHPUnit tests to Pest tests in a matter of seconds.
So, if you have a test like this:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}You can run ./vendor/bin/pest --drift and Pest will automatically convert your PHPUnit test to a Pest test:
test('true is true', function () {
expect(true)->toBeTrue();
});Thank you for reading about Pest 2.9's new features! If you're considering a testing framework for your next project, here's why you should give Pest a try: Why Pest →
File: pest-v4-is-here-now-with-browser-testing.md
title: Pest v4 Is Here — Now with Browser Testing
To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: Upgrade Guide →.
- Browser Testing
- Smoke Testing
- Visual Regression Testing
- Test Sharding
- Type Coverage Is Much Faster
- Profanity Checking
- On Top of PHPUnit 12
Pest v4 Is Here — Now with Browser Testing
Today, we're thrilled to announce the release of Pest v4, bringing the biggest testing upgrade yet: powerful Browser Testing. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.
Here is the creator of Pest, Nuno Maduro, demoing the new browser testing features in Pest v4 at Laracon US:
Here is an example of Browser Testing using Laravel:
it('may reset the password', function () {
// access any laravel testing helpers...
Notification::fake();
// access to the database — using the RefreshDatabase trait (even sqlite in memory...)
$this->actingAs(User::factory()->create());
$page = visit('/sign-in') // visit on a real browser...
->on()->mobile() // or ->desktop(), ->tablet(), etc...
->inDarkMode(); // or ->inLightMode()
$page->assertSee('Sign In')
->click('Forgot Password?')
->type('email', '[email protected]')
->press('Send Reset Link')
->assertSee('We have emailed your password reset link!')
->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()
Notification::assertSent(ResetPassword::class);
});With Pest v4's browser testing, you can:
- Seamlessly use Laravel features like
Event::fake(),assertAuthenticated(), and model factories - Use
RefreshDatabase, even with SQLite in-memory databases, to ensure a clean state for each test - Test on multiple browsers (Chrome, Firefox, Safari)
- Test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)
- Switch color schemes (light/dark mode)
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)
- Run parallel browser tests for dramatically faster suites
- Take screenshots or pause tests for debugging
- …all with the elegance of Pest syntax
- Playwright-based — modern, fast, and reliable
To get started with browser testing in Pest, you need to install the Pest Browser Plugin:
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright installAfter, you may use the visit() function anywhere. Finally, running this test is as simple as executing ./vendor/bin/pest in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.
Smoke Testing
Smoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.
$routes = ['/', '/about', '/contact'];
visit($routes)->assertNoSmoke();
// assertNoSmoke() is a shorthand for:
// - assertNoJavascriptErrors()
// - assertNoConsoleLogs()Visual Regression Testing
Want to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the assertScreenshotsMatches() assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.
$pages = visit(['/', '/about', '/contact']);
$pages->assertScreenshotsMatches();This is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the Browser Testing documentation for a complete guide on how to get started.
Test Sharding
Pest v4 introduces Test Sharding, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.
This feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.
To get started with Test Sharding, you can use the --shard option when running Pest:
# GitHub Workflow One
./vendor/bin/pest --shard=1/4
# GitHub Workflow Two
./vendor/bin/pest --shard=2/4
# GitHub Workflow Three
./vendor/bin/pest --shard=3/4
# GitHub Workflow Four
./vendor/bin/pest --shard=4/4You may combine this with the --parallel option to run your tests in parallel, and this way trully maximize your test suite execution speed:
./vendor/bin/pest --shard=1/4 --parallelFinally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the matrix strategy to define multiple jobs that run different shards:
strategy:
matrix:
shard: [1, 2, 3, 4]
name: Tests (Shard ${{ matrix.shard }}/4)
steps:
- name: Run tests
run: pest --parallel --shard ${{ matrix.shard }}/5Type Coverage Is Much Faster
Remember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.
Type coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.
In addition, Type Coverage now supports Sharding. This means you can run type coverage with the --shard option, just like you do with your tests.
Profanity Checking
Pest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.
You can enable profanity checking by adding the --profanity option when running Pest:
./vendor/bin/pest --profanityTo start using Pest's Profanity plugin, you need to require the plugin via Composer.
composer require pestphp/pest-plugin-profanity --devAfter requiring the plugin, you may utilize the --profanity option to generate a report of your profanity.
./vendor/bin/pest --profanityIf any of your files contain profanity, they will be highlighted in red and displayed using their respective line
numbers and the profane word(s) that have been found.
As an example, pr31(f*ck) means that the word "fuck" was found on line 31.
To learn more about the Profanity plugin and how to configure it, check out the Profanity documentation.
Skip Locally or On CI
Pest v4 introduces the ability to conditionally skip tests based on the environment. You can use skipLocally() to skip tests when running locally, or skipOnCi to skip tests when running on a CI server.
it('does not run locally', function () {
// This test will be skipped when running locally
})->skipLocally();
it('does not run on CI', function () {
// This test will be skipped when running on a CI server
})->skipOnCi();Miscellaneous Improvements
- You may now use
skipLocally()orskipOnCito conditionally skip tests based on the environment. - The
not->toHaveSuspiciousCharacters()arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on thephparch preset. - The expectation
toBeSlughas been added to help you validate that a string is a valid slug.
On Top of PHPUnit 12
Pest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the PHPUnit 12 release announcement.
Thanks To You, Pest v4 Is Here!
There's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our installation guide for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our upgrade guide.
Thank you for your continued support and feedback. We can't wait to see what you build with Pest v4!
Thank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: Installation →
File: pest3-now-available.md
title: Pest v3 Now Available
Pest v3 Now Available
Today, we're thrilled to announce the release of Pest 3. As we announced at Laracon US, Pest 3 introduces Mutation Testing, Arch Presets, Team Management, New Configuration API, multiple improvements to Architectural Testing & more.
Check out Pest's creator, Nuno Maduro, live demonstrating what's new in Pest 3:
Below, we'll cover all the juicy details about this release. And as usual, you can find the upgrade guide in our website.
- Mutation Testing: An innovative new technique that introduces small changes to your code to see if your tests catch them.
- Arch Presets: A set of predefined rules that you can use to test your application's architecture.
- Team Management: A new feature that allows you to manage tasks and todos with your team directly from the console.
- Nested Describes: You can now nest describe blocks within other describe blocks.
- New Configuration API: A new configuration API that is more intuitive and easier to use.
- More Architectural Testing Improvements:
toUseStrictEquality,toHaveMethodsDocumented,->not->toHaveProtectedMethods, and more. - And Much More...: Constants in Type Coverage, static analysis improvements, and more.
Mutation Testing
Mutation Testing is an innovative new technique that introduces small changes (mutations) to your code to see if your tests catch them. This ensures you’re testing your application thoroughly, beyond just achieving code coverage and more about the actual quality of the tests. It’s a great way to identify weaknesses in your test suite and improve quality.
To get started with mutation testing, head over to your test file, and be specific about which part of your code your test covers using the covers() function.
covers(TodoController::class);
it('list todos', function () {
$this->getJson('/todos')->assertStatus(200);
});Then, run Pest PHP with the --mutate option to start mutation testing.
./vendor/bin/pest --mutate
# or in parallel...
./vendor/bin/pest --mutate --parallelPest will then re-run your tests against "mutated" code and see if the tests are still passing. If a test is still passing against a mutation, it means that the test is not covering that specific part of the code. As, as result, Pest will output the mutation and the diff of the code.
UNTESTED app/Http/TodoController.php > Line 44: ReturnValue - ID: 76d17ad63bb7c307
class TodoController {
public function index(): array
{
// pest detected that this code is untested because
// the test is not covering the return value
- return Todo::all()->toArray();
+ return [];
}
}
Mutations: 1 untested
Score: 33.44%Once you have identified the untested code, you can write additional tests to cover it.
covers(TodoController::class);
it('list todos', function () {
+ Todo::factory()->create(['name' => 'Buy milk']);
- $this->getJson('/todos')->assertStatus(200);
+ $this->getJson('/todos')->assertStatus(200)->assertJson([['name' => 'Buy milk']]);
});Then, you can re-run Pest with the --mutate option to see if the mutation is now "tested" and covered.
Mutations: 1 tested
Score: 100.00%The higher the mutation score, the better your test suite is. A mutation score of 100% means that all mutations were "tested", which is the goal of mutation testing.
Now, if you see "untested" or "uncovered" mutations, or are a mutation score below 100%, typically means that you have missing tests or that your tests are not covering all the edge cases.
Our plugin is deeply integrated into Pest PHP. So, each time a mutation is introduced, Pest PHP will:
- Only run the tests covering the mutated code to speed up the process.
- Cache as much as possible to speed up the process on subsequent runs.
- If enabled, use parallel execution to run multiple tests in parallel to speed up the process.
There is so much more to explore with Mutation Testing, like @pest-mutate-ignore or --mutate --everything. You can learn more about it in our Mutation Testing section.
Arch Presets
As you may know, Architecture testing enables you to specify expectations that test whether your application adheres to a set of architectural rules, helping you maintain a clean and sustainable codebase.
It's one of the most popular features of Pest, and with Pest 3, we're introducing Arch Presets. Arch Presets are a set of predefined architectural rules that you can use to test your application's architecture. These presets are designed to help you get started with architecture testing quickly and easily.
Here are the available Arch Presets in Pest 3:
`php`
The php preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It avoids the usage of die, var_dump, and similar functions, and ensures you are not using deprecated PHP functions. source code
arch()->preset()->php();`security`
The security preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are not using code that could lead to security vulnerabilities, such as eval, md5, and similar functions. source code
arch()->preset()->security();`laravel`
The laravel preset is a predefined set of expectations that can be used on Laravel projects.
It ensures you project's structure is following the well-known Laravel conventions, such as controllers only have index, show, create, store, edit, update, destroy as public methods and are always suffixed with Controller and so on. source code
arch()->preset()->laravel();`strict`
The strict preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are using strict types in all your files, that all your classes are final, and more. source code
arch()->preset()->strict();`relaxed`
The relaxed preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It is the opposite of the strict preset, ensuring you are not using strict types in all your files, that all your classes are not final, and more. source code
arch()->preset()->relaxed();Just like regular architecture tests, you may ignore specific expectation targets using the ignoring() method.
arch()->preset()->security()->ignoring('md5');
arch()->preset()->laravel()->ignoring(User::class);To get started with Arch Presets, please refer to our Architecture Testing section.
Team Management
Pest 3 also introduces Team Management, a new feature that allows you to manage tasks and todos with your team directly from the console. With Team Management, you can create, assign, and track tasks, as well as view the status of each task.
To get started with team management in Pest, you need to specify the project's URL in your Pest.php configuration file. This URL will be used to link todos to the corresponding project management system.
pest()->project()->github('my-organization/my-repository');If you are using a different version control system, you can use the gitlab, bitbucket, jira, or custom methods instead.
Finally, you can create todos by using the todo() method. Also, you may use the assignee, issue, arguments to assign todos to specific team members or link them to issues in your project management system.
it('has a contact page', function () {
//
})->todo(assignee: '[email protected]', issue: 123);Also, it is often helpful to provide additional context for a todo. Pest allows you to write notes for a todo by providing a string to the note argument of the todo() method.
it('has a contact page', function () {
//
})->todo(note: <<<NOTE
Given I am a user
When I visit the contact page
Then I should see a contact form
NOTE);Once a todo is completed, you can mark it as work in progress by using the wip() method or mark it as done by using the done() method.
it('has a contact page', function () {
//
})->wip(assignee: '[email protected]', issue: 123); // or ->done()Finally, you can view todos separately from the rest of your test suite by including the --todos option when running Pest. You can also filter todos by assignee by providing their name to the --assignee option, or filter todos by issue by providing the issue number to the --issue option.
./vendor/bin/pest --todos --assignee=taylor # or --issue=123There is so much more to explore with Team Management, you can learn more about it in our Team Management section.
Nested Describes
In Pest 3, you can now nest describe blocks within other describe blocks. This allows you to group tests more effectively and keep your test suite organized.
describe('home', function () {
beforeEach(function () {
//
});
it('can be visited', function () {
//
});
describe('footer', function () {
it('contains a link to the contact page', function () {
//
});
});
});New Configuration API
Pest 1 / Pest 2's configuration API was a little bit confusing, the uses() function that was originally made only for having the $this variable within closure bound to the test case instance, ended up being used for pretty much everything.
In Pest 3, we've introduced a new configuration API that is more intuitive and easier to use. The new configuration API is based on the pest() function, which allows you to configure Pest using a fluent and expressive API.
Note: the
uses()function is still available in Pest 3, and we don't have plans to remove it. However, we recommend using the new configuration API for new projects.
-uses(TestCase::class)->in(__DIR__);
+pest()->extends(TestCase::class);
-uses(TestCase::class, RefreshDatabase::class)->in('Features');
+pest()->extends(TestCase::class)->use(RefreshDatabase::class)->in('Features');
-uses()->compact();
+pest()->printer()->compact();And of course, any method that was available on the uses() API, like ->beforeEach() or ->group() is still available on the new pest() configuration API; we've just made it more intuitive and easier to use.
More Architectural Testing Improvements
New Expectations
Again, Pest comes with a bunch of new architectural expectations and improvements. Some of them are already being used in the new Arch Presets, but you can use them individually as well.
toUseStrictEquality()- Asserts that a target uses strict equality.===instead of==.toHaveMethodsDocumented()- Asserts that a class has all its methods documented.toHavePropertiesDocumented()- Asserts that a class has all its properties documented.toHaveFileSystemPermissions()- Asserts that a file has the expected file system permissions.toHaveLineCountLessThan- Asserts that a file has less than a given number of lines.toHaveMethods()- Asserts that a class has the expected methods.not->toHavePrivateMethodsBesides()- Asserts a class only "allows" the given private methods.not->toHavePrivateMethods()- Asserts that a class does not have private methods.not->toHaveProtectedMethodsBesides()- Asserts a class only "allows" the given protected methods.not->toHaveProtectedMethods()- Asserts that a class does not have protected methods.not->toHavePublicMethodsBesides()- Asserts a class only "allows" the given public methods.not->toHavePublicMethods()- Asserts that a class does not have public methods.toUseTrait()- Asserts that a class uses the given trait.toUseTraits()- Asserts that a class uses the given traits.
You may check all existing architectural expectations in our Architecture Testing section.
Tear Down Improvements
As you may know, Pest allows you to run a specific "teardown" callback after each test using the afterEach() method. This is useful for cleaning up resources or resetting state between tests.
afterEach(function () {
// This will run after each test...
});In Pest 3, we've introduced a new after() method that allows you to run a specific "teardown" callback after a specific test or group of tests using describe.
it('may list todos', function () {
//
})->after(function () {
// This will run after this test only...
});To read more about hooks, please refer to our Hooks section.
Miscellaneous Improvements
Because Pest 3 is based on PHPUnit 11, you can now use any PHPUnit 11 feature within Pest. Also, Pest 3 also comes with a bunch minor bug-fixes and improvements, below are some of the them:
- FEAT: Type Coverage now checks for missing types on constants.
- FEAT: Better error messages when static closures are used on tests + wrong arguments on datasets.
- FEAT: Adds basic support for static analysis tools within test closures.
- FEAT: Overall static analysis improvements on expectations and the entire API surface.
- FEAT: Possibility of deleting the
phpunit.xmlfile and having Pest working out of the box. - FIX: Exit code being computed incorrectly when using
--fail-on-xxxCLI options. - FIX: Describe blocks now support more than one method call when chaining methods.
- FIX: Runtime exceptions before the first test are now caught and displayed.
- FIX: Having coverage report failing with
--min=100option when result less than 100 but bigger than 99.5. - And much more...
There's never been a better time to dive in into testing and start using Pest. If you're ready to get started with Pest 3 right away, check out our installation guide for step-by-step instructions. And if you're currently using Pest 2, we've got you covered with detailed upgrade instructions in our upgrade guide.
Thank you for your continued support and feedback. We can't wait to see what you build with Pest 3!
Thank you for reading about Pest 3.0's new features! If you're considering a testing framework for your next project, here's why you should give Pest a try: Why Pest →
File: plugins.md
title: Plugins
Plugins
In this section, we will discuss the official and community developed plugins that we endorse. Plugins primarily offer namespaced functions, console commands, custom expectations, and additional command-line options to augment the default Pest experience.
If you are a plugin developer, please consult our documentation on creating plugins for more information on how to create Pest plugins.
The following plugins are maintained by the Pest team:
Faker
Source code: github.com/pestphp/pest-plugin-faker
To start using Pest's Faker plugin, you need to require the plugin via Composer.
composer require pestphp/pest-plugin-faker --devAfter installing the plugin, you may utilize the namespaced fake function to generate fake data for your tests.
use function Pest\Faker\fake;
it('generates a name', function () {
$name = fake()->name; // random name...
//
});You can also designate the "locale" that should be utilized by the fake() function by providing the locale to the function.
use function Pest\Faker\fake;
it('generates a portuguese name', function () {
$name = fake('pt_PT')->name; // Nuno Maduro
//
});To learn more about Faker, including comprehensive details about the API it provides, please consult its official documentation.
Laravel
Source code: github.com/pestphp/pest-plugin-laravel
To start using Pest's Laravel plugin, you need to require this plugin via Composer.
composer require pestphp/pest-plugin-laravel --devThis plugin adds additional Artisan commands and functions to the default Pest installation. For example, to generate a new test in the tests/Feature directory, you can now utilize the pest:test Artisan command.
php artisan pest:test UsersTestYou may provide the --unit option when creating a test to place the test in the tests/Unit directory.
php artisan pest:test UsersTest --unitExecuting the pest:dataset Artisan command will create a fresh dataset in the tests/Datasets directory.
php artisan pest:dataset EmailsAs you may know, Laravel provides a variety of assertions you can take advantage of in your feature tests. When using Pest's Laravel plugin, you may access all of those assertions as you typically would.
it('has a welcome page', function () {
$this->get('/')->assertStatus(200);
});In addition, with the assistance of this plugin, it is possible for you to bypass the $this variable while using namespaced functions such as actingAs, get, post and delete.
use function Pest\Laravel\{get};
it('has a welcome page', function () {
get('/')->assertStatus(200);
// same as $this->get('/')...
});To illustrate this convenient feature using another example, we can write a test acting as an authenticated user accessing the restricted dashboard page.
use App\Models\User;
use function Pest\Laravel\{actingAs};
test('authenticated user can access the dashboard', function () {
$user = User::factory()->create();
actingAs($user)->get('/dashboard')
->assertStatus(200);
});As you may expect, all of the assertions that were previously accessible via $this-> are available as namespace functions.
use function Pest\Laravel\{actingAs, get, post, delete, ...};You can find the full testing documentation on the Laravel website: laravel.com/docs/12.x/testing.
Livewire
Source code: github.com/pestphp/pest-plugin-livewire
To install Pest's Livewire plugin, you need to require the plugin via Composer.
composer require pestphp/pest-plugin-livewire --devAfter installing the plugin, you may use the livewire namespaced function to access your Livewire components.
use function Pest\Livewire\livewire;
it('can be incremented', function () {
livewire(Counter::class)
->call('increment')
->assertSee(1);
});
it('can be decremented', function () {
livewire(Counter::class)
->call('decrement')
->assertSee(-1);
});In this section, we have seen how plugins can enhance your Pest experience. Now, let's see how you can manage your team's tasks and responsibilities using Pest: Team Management
File: profanity.md
title: Profanity
Profanity
Source code: github.com/pestphp/pest-plugin-profanity
The Profanity plugin will scan your codebase for profanity in places like comments, constants and properties. This can
help you maintain a more professional and respectful codebase. As developers, we've all faced moments of frustration,
whether it be debugging a persistent issue or trying to decipher confusing code written by someone else. These moments
can sometimes result in the inclusion of profanity in your code.
To start using Pest's Profanity plugin, you need to require the plugin via Composer.
composer require pestphp/pest-plugin-profanity --devAfter requiring the plugin, you may utilize the --profanity option to generate a report of your profanity.
./vendor/bin/pest --profanityProfanity does not require you to write any tests. Instead, it analyses your codebase and generates a report of your
profanity. This report will display a list of files and their corresponding profanity results.
If any of your files contain profanity, they will be highlighted in red and displayed using their respective line
numbers and the profane word(s) that have been found.
As an example, pr31(fuck) means that the word "fuck" was found on line 31.
Specific Language
Often, a codebase is a single language, so you would only want to flag profanity for the language of the codebase. To
do this, you can use the --language option:
./vendor/bin/pest --profanity --language=enIf needed, you can pass in multiple comma separated languages too:
./vendor/bin/pest --profanity --language=en,daWe currently support: ar, da, en, es, it, ja, nl, pt_BR and ru. If no language is specified,
we use en as the default.
Include Words
There may be times when you want to flag certain words specific to your application as profane. To do this, you can use
the --include option:
./vendor/bin/pest --profanity --include=elephpantExclude Words
There may be times when you want to exclude certain words from being flagged up as profane. To do this, you can use the--exclude option:
./vendor/bin/pest --profanity --exclude=elephpantCompact Output
Often, when checking for profanity, you only want to see files that do contain profanity. To do this, you can use
the --compact option:
./vendor/bin/pest --profanity --compactDifferent Formats
In addition, Pest supports reporting the profanity results to a specific file:
./vendor/bin/pest --profanity --output=my-report.jsonExclude Lines
If you wish to exclude certain lines that contain profanity from being flagged, without excluding words from the
whole application, you can tell the checker to ignore certain lines:
const string Fuck; // @pest-ignore-profanityIn this chapter, we have discussed Pest's Profanity plugin and how it can be used to help you maintain a professional
codebase. In the following chapter, we will explore additional CLI options that Pest
provides: CLI API Reference
File: skipping-tests.md
title: Skipping Tests
Skipping Tests
During the development process, there may be times when you need to temporarily disable a test. Rather than commenting out the code, we recommended using the skip() method.
it('has home', function () {
//
})->skip();When running your tests, Pest will inform you about any tests that were skipped.
You may also provide the reason for skipping the test, which Pest will display when running your tests.
it('has home', function () {
//
})->skip('temporarily unavailable');In addition, there may be times when you want to skip a test based on a given condition. In these cases, you may provide a boolean value as the first argument to the skip() method. This test will only be skipped if the boolean value evaluates to true.
it('has home', function () {
//
})->skip($condition == true, 'temporarily unavailable');You may pass a closure as the first argument to the skip() method to defer the evaluation of the condition until the beforeEach() hook of your test case has been executed.
it('has home', function () {
//
})->skip(fn () => DB::getDriverName() !== 'mysql', 'db driver not supported');You may also skip tests based on the environment in which they are running using the skipLocally() or skipOnCi() methods.
it('has home', function () {
//
})->skipLocally(); // or skipOnCi()To skip a test on a particular operating system, you can make use of the skipOnWindows(), skipOnMac(), or skipOnLinux().
it('has home', function () {
//
})->skipOnWindows(); // or skipOnMac() or skipOnLinux() ...Alternatively, you can skip a test on all operating systems except one by using onlyOnWindows(), onlyOnMac(), or onlyOnLinux().
it('has home', function() {
//
})->onlyOnWindows(); // or onlyOnMac() or onlyOnLinux() ...Sometimes, you may want to skip a test on a specific PHP version. In these cases, you may use the skipOnPhp() method.
it('has home', function () {
//
})->skipOnPhp('>=8.0.0');The valid operators for the skipOnPhp() method are >, >=, <, and <=.
Finally, you may even invoke the skip() method within your beforeEach() hook to conveniently skip an entire test file.
beforeEach()->skip(); // or skipOnCi(), etc...Creating todos
You might want to add a couple of empty tests to make sure you don't forget to add them later. The todo() can be useful in this situation.
it('has home', function () {
//
})->todo();As your codebase expands, it's advisable to consider enhancing the speed of your test suite. To assist you with that, we offer comprehensive documentation on optimizing your test suite: Optimizing Tests
File: snapshot-testing.md
title: Snapshot Testing
Snapshot Testing
Snapshot Testing is a great way to test your code by comparing by the given expectation value to a previously stored snapshot of the same value. This is useful when you want to ensure that your code is not changing its output unexpectedly.
As example, let's say you have a string response coming from an API. You can use snapshot testing to ensure that the response is not changing unexpectedly.
it('has a contact page', function () {
$response = $this->get('/contact');
expect($response)->toMatchSnapshot();
});The first time you run this test, it will create a snapshot file - at tests/.pest/snapshots - with the response content. The next time you run the test, it will compare the response with the snapshot file. If the response is different, the test will fail. If the response is the same, the test will pass.
In addition, the given expectation value doesn't have to be a response; it can be anything. For example, you can snapshot an array:
$array = /** Fetch array somewhere */;
expect($array)->toMatchSnapshot();And of course, you can "rebuild" the snapshots at any time by using the --update-snapshots option:
./vendor/bin/pest --update-snapshotsHandling Dynamic Data
Sometimes, the expected value may contain dynamic data that you cannot control, such as CSRF tokens in a form. In those cases, you can use Expectation Pipes to replace that data. Here is an example:
expect()->pipe('toMatchSnapshot', function (Closure $next) {
if (is_string($this->value)) {
$this->value = preg_replace(
'/name="_token" value=".*"/',
'name="_token" value="my_test"',
$this->value
);
}
return $next();
});In this chapter, we've seen how powerful snapshot testing is. In the following chapter, we will dive into Pest's custom helpers: Custom Helpers
File: stress-testing.md
title: Stress Testing
Stress Testing
Stress Testing is a type of testing that inspects the stability and reliability of your application under realistic or extreme conditions — depending on the scenario you setup. For example, you can use stress testing to verify that your application can handle a large number of requests or that it can handle a large amount of data.
In Pest, you can combine the power of Stress Testing with the Expectation API ensuring no stability and reliability regressions over time. This can be useful to verify that your application is stable and reliable after a new release, or after a new deployment.
Behind the scenes, this project utilizes k6, a powerful open-source load testing tool for evaluating the performance of APIs, microservices, and websites. k6 is licensed under the AGPL-3.0 License, and the k6 binary is downloaded when the plugin is used for the first time.
To start using Pest's Stress Testing plugin (mostly known as Stressless), you need to require the stressless plugin via Composer.
composer require pestphp/pest-plugin-stressless --devAfter requiring the plugin, you may start using it in two different ways:
- Using the
stresscommand: It's useful when you want to quickly stress test a URL, without setting expectations on the result. - Using the
stress()function: It's useful when you want to stress test a URL and set expectations on the result.
Testing external domain or local IP Address? When load testing a domain from an external network, you get a realistic picture of how your application performs under typical user loads. This includes factors like network latency and real-world internet traffic. However, when testing a local IP address within your network, the focus is on understanding the performance of your internal infrastructure in a controlled environment, without external variables like internet or DNS resolution times. It's particularly useful for identifying potential bottlenecks within your own network or server and for performance tuning of internal applications or servers. This includes tasks such as configuring PHP FPM more effectively, etc.
The Stress Command
The stress command is useful when you want to quickly stress test a URL, analyze the result, and all without setting expectations on the result. It's the quickest way to launch a stress test, and it happens directly on the terminal.
To get started, you may use the stress command and provide the URL you wish to stress test:
./vendor/bin/pest stress example.comBy default, the stress test duration will be 5 seconds. However, you may customize this value using the --duration option:
./vendor/bin/pest stress example.com --duration=5In addition, the number of concurrent requests will be 1. However, you may also customize this value using the --concurrency option:
./vendor/bin/pest stress example.com --concurrency=5The concurrency value represents the number of concurrent requests that will be made to the given URL. For example, if you set the concurrency to 5, Pest will constantly make 5 concurrent requests to the given URL until the stress test duration is reached.
You may want to be mindful of the number of concurrent requests you configure. If you configure too many concurrent requests, you may overwhelm your application, server or hit rate limits / firewalls.
If you want to specify the http method used for the stress test, you can use one of the provided delete, get, head, options, patch, put or post options. Using options, patch and put options, you can specify an optional payload argument to be used in the requests. Using post option, you are required to provide the payload argument.
./vendor/bin/pest stress example.com/articles
# or
./vendor/bin/pest stress example.com/articles --get
# or
./vendor/bin/pest stress example.com/articles --head
# or
./vendor/bin/pest stress example.com/articles --options
# or
./vendor/bin/pest stress example.com/articles --options='{"name": "Nuno"}'
# or
./vendor/bin/pest stress example.com/articles/1 --patch
# or
./vendor/bin/pest stress example.com/articles/1 --patch='{"name": "Nuno"}'
# or
./vendor/bin/pest stress example.com/articles --put
# or
./vendor/bin/pest stress example.com/articles --put='{"name": "Nuno"}'
# or
./vendor/bin/pest stress example.com/articles --post='{"name": "Nuno"}'
# or
./vendor/bin/pest stress example.com/articles/1 --deleteOnce the stress test is completed, Pest will display a summary of the stress test result.
The Stress function
Once you start understanding how stress testing works, you may want to start setting expectations on the stress test result. For example, you may want to verify that the average response time is always less than 100ms, and this is where the stress() function comes in.
To get started, simply create a regular test and use the stress() function to stress test a given URL:
<?php
use function Pest\Stressless\stress;
it('has a fast response time', function () {
$result = stress('example.com');
expect($result->requests()->duration()->med())->toBeLessThan(100); // < 100.00ms
});By default, the stress test duration will be 10 seconds. However, you may customize this value using the for()->seconds() method:
$result = stress('example.com')->for(5)->seconds();In addition, the number of concurrent requests will be 1. However, you may also customize this value using the concurrently method:
$result = stress('example.com')->concurrently(requests: 2)->for(5)->seconds();At any time, you may dd the stress test result to see its details like if you were using the stress command):
$result = stress('example.com')->dd();
//->dump();
//->verbosely();If you want to specify the http method used for the stress test, you can use one of the provided delete, get, head, options, patch, put or post methods.
Using options, patch and put methods, you can specify an optional payload argument to be used in the requests.
Using post method, you are required to provide the payload argument.
$result = stress('example.com/articles/1')->delete();
// or
$result = stress('example.com/articles')->get();
// or
$result = stress('example.com/articles')->head();
// or
$result = stress('example.com/articles')->options();
// or
$result = stress('example.com/articles')->options(["name" => "Nuno"]);
// or
$result = stress('example.com/articles/1')->patch();
// or
$result = stress('example.com/articles/1')->patch(["name" => "Nuno"]);
// or
$result = stress('example.com/articles')->put();
// or
$result = stress('example.com/articles')->put(["name" => "Nuno"]);
// or
$result = stress('example.com/articles')->post(["name" => "Nuno"]);If you want to specify request headers, you can use the provided headers method.
$result = stress('example.com/articles')->headers([
'Authorization' => 'Bearer SecretToken',
])->get();The stress() function return the stress test result, which you can use to set expectations. Here is the list of available methods:
Request Duration
Returns the overall request duration in milliseconds.
$result->requests()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests Count
Returns the number of requests made.
$result->requests()->count();Requests Rate
Returns the number of requests made per second.
$result->requests()->rate();Requests Failed Count
Returns the number of requests that failed.
$result->requests()->failed()->count();Requests Failed Rate
Returns the number of requests that failed per second.
$result->requests()->failed()->rate();Requests Time To First Byte Duration / TTFB
Returns the request time to first byte duration in milliseconds.
$result->requests()->ttfb()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests DNS Lookup Duration
This metric is affected by the network latency between the client and the DNS server.
Returns the request DNS lookup duration in milliseconds.
$result->requests()->dnsLookup()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests TLS Handshaking Duration
This metric is affected by the network latency between the client and the server.
Returns the request TLS handshaking duration in milliseconds.
$result->requests()->tlsHandshaking()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests Download Duration
This metric is affected by the network latency between the client and the server.
Returns the request download duration in milliseconds.
$result->requests()->download()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests Download Data Count
Returns the request download data count in bytes.
$result->requests()->download()->data()->count();Requests Download Data Rate
Returns the request download data rate in bytes per second.
$result->requests()->download()->data()->rate();Requests Upload Duration
This metric is affected by the network latency between the client and the server.
Returns the request upload duration in milliseconds.
$result->requests()->upload()->duration()->med();
// ->min();
// ->max();
// ->p90();
// ->p95();Requests Upload Data Count
Returns the request upload data count in bytes.
$result->requests()->upload()->data()->count();Requests Upload Data Rate
Returns the request upload data rate in bytes per second.
$result->requests()->upload()->data()->rate();Test Run Concurrency
Returns the number of concurrent requests made during the stress test, which is the value you set using the --concurrency option or the concurrently method.
$result->testRun()->concurrency();Test Run Duration
Returns the duration of the stress test, which is the value you set using the --duration option or the for()->seconds() method.
$result->testRun()->duration();Here, we've seen how to use Pest's Stress Testing plugin (aka stressless) to stress test a given URL and set expectations on the result. Moving on, let's explore how to test the coverage of your testing code: Test Coverage
File: support-policy.md
title: Support Policy
Support Policy
We strive to resolve all reported bugs or issues to the best of our abilities as an open-source project. Nevertheless, we cannot ensure a fixed resolution time or guarantee the availability of a fix for every problem.
Bug fixes will be available for outdated versions for a duration of two years following the latest version's release. The previous version will be regarded as outdated once a new version of Pest is released.
| Major Version | PHP Compatibility | Initial Release | Bug Fixes Until |
|---|---|---|---|
| Pest 4 | >= PHP 8.3 | August 21, 2025 | To be determined |
| Pest 3 | >= PHP 8.2 | September 9, 2024 | August 21, 2027 |
| Pest 2 | >= PHP 8.1 | March 20, 2023 | September 9, 2026 |
| Pest 1 | >= PHP 7.3 | January 7, 2021 | March 20, 2025 |
Pest adheres to semantic versioning principles, where the version number x.y.z conveys the following information:
- When issuing bug fixes, the
znumber is incremented (e.g., 4.10.2 to 4.10.3). - When adding new non-breaking features or improvements, the
ynumber is incremented (e.g., 4.10.2 to 4.12.0). - When introducing breaking changes, the
xnumber is incremented (e.g., 4.10.2 to 5.0.0).
As maintainers of testing frameworks, we take breaking changes very seriously. Our goal is to deliver robust, cutting-edge features without disrupting the community's test suites. This commitment is why upgrading from Pest 2 to Pest 3 was as simple as updating your composer.json file. Similarly, the transition to Pest 4 has been designed to be just as seamless, ensuring an effortless upgrade experience for our users.
In the next chapter, we will explore the process of upgrading between major versions via our upgrade guide: Upgrade Guide
File: team-management.md
title: Team Management
Team Management
With Pest, you can manage tasks and todos with your team directly from the console. You can create, assign, and track tasks, as well as view the status of each task.
Setting Up Project
To get started with team management in Pest, you need to specify the project's URL in your Pest.php configuration file. This URL will be used to link todos to the corresponding project management system.
pest()->project()->github('my-organization/my-repository');If you are using a different version control system, you can use the gitlab, bitbucket, jira, or custom methods instead.
Creating Todos
Typically, todos are linked to one or more tests that need to be passing. As such, tests can be used to track the progress of your todos / tasks. Pest provides a simple way to create todos by using the todo() method.
it('has a contact page', function () {
//
})->todo();When running your tests, Pest will inform you about any tests that are todos so you don't forget and see them in the test results.
If you have one or more todos, you may want to view them separately from the rest of your test suite. You can do this by including the --todos option when running Pest.
./vendor/bin/pest --todosAssigning Todos
In some cases, you may want to assign a todo to a specific team member. Pest allows you to assign a todo to a specific team member by providing their name to the assignee argument of the todo() method.
it('has a contact page', function () {
//
})->todo(assignee: 'nunomaduro');You may assign multiple assignees by providing an array of names to the assignee argument. Also, you may filter todos by assignee by providing their name to the --assignee option when running Pest.
./vendor/bin/pest --todos --assignee=nunomaduroSet Corresponding Issues
Sometimes, todos are linked to issues in your project management system. Pest allows you to set the corresponding issue to a todo by providing the issue number to the issue argument of the todo() method.
it('has a contact page', function () {
//
})->todo(issue: 123);Just like with assignees, you may set multiple issues by providing an array of issue numbers to the issue argument. Also, you may filter todos by issue by providing the issue number to the --issue option when running Pest.
./vendor/bin/pest --todos --issue=123Set Corresponding PRs
Sometimes, todos are linked to pull requests in your version control system. Pest allows you to set the corresponding pull request to a todo by providing the pull request number to the pr argument of the todo() method.
it('has a contact page', function () {
//
})->todo(pr: 123);Just like with assignees, you may set multiple pull requests by providing an array of pull request numbers to the pr argument. Also, you may filter todos by pull request by providing the pull request number to the --pr option when running Pest.
./vendor/bin/pest --todos --pr=123Writing Notes for Todos
It is often helpful to provide additional context for a todo. Pest allows you to write notes for a todo by providing a string to the note argument of the todo() method.
it('has a contact page', function () {
//
})->todo(note: <<<NOTE
Given I am a user
When I visit the contact page
Then I should see a contact form
NOTE);The notes will be displayed below the todo in the test results.
Marking Todos as Work in Progress
Once a todo is completed, you can mark it as work in progress by using the wip() method. This method will remove the todo status from the test and mark it as a regular test while keeping all the context like assignees, issues, etc.
it('has a contact page', function () {
//
})->wip(assignee: 'nunomaduro', issue: 123);Marking Todos as Done
Once a todo is completed, you can mark it as done by using the done() method. This method will remove the todo status from the test and mark it as a regular test while keeping all the context like assignees, issues, etc.
it('has a contact page', function () {
//
})->done(assignee: 'nunomaduro', issue: 123);Combining Todos with Assignees, Issues, and PRs
You can combine todos with assignees, issues, and PRs to provide additional context and track the progress of your todos. This can be done using the describe group, and the todo, assignee, issue, and pr methods.
describe('contacts', function () {
it('has a contact page', function () {
//
}))->issue(123); // or ->pr(123) etc
it('has a contact form', function () {
//
})->done(pr: 567);
})->wip(assignee: 'nunomaduro');Next, we will explore browser testing with Pest, which allows you to test your application's user interface in a real browser environment. This is particularly useful for testing complex interactions and ensuring that your application behaves as expected from a user's perspective: Browser Testing →
File: test-coverage.md
title: Test Coverage
Test Coverage
Requires XDebug 3.0+ or PCOV.
Test coverage (or code coverage) is a metric used to measure the percentage of code that is executed during testing. This can help developers identify parts of their code that may not be tested or that have low coverage, indicating a potential risk for bugs and other issues.
Typically, the essential configuration for gathering code coverage is already present in the phpunit.xml file provided by frameworks, or is generated by executing the ./vendor/bin/pest --init command. If code coverage configuration is not present in your phpunit.xml file, you can add your own configuration to specify the paths in your project that should receive code coverage reporting.
...
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
...In addition to configuring your phpunit.xml file, you will also need to install XDebug 3.0+ or PCOV to generate a code coverage report. When utilizing XDebug, the XDEBUG_MODE environment variable must be configured as coverage.
Once you have configured your code coverage settings and installed a coverage driver, generating a code coverage report becomes effortless with the use of the --coverage option.
./vendor/bin/pest --coverageBy utilizing the --coverage option, the test suite runs normally, but with the additional feature of displaying a list of project files and their corresponding coverage results.
If there are any uncovered lines in your current test suite, they will be highlighted in red and displayed using their respective line numbers. Multiple uncovered lines will be displayed with two dots (..) between them. For instance, if there is missing coverage between lines 52 and 60, you will see 52..60 instead of just 52 in red.
Minimum Threshold Enforcement
To ensure comprehensive testing and maintain code quality, it is crucial to set minimum threshold values for coverage results. In Pest, you can use the --coverage and --min or --exactly options to define the minimum threshold values for coverage results. If the specified thresholds are not met, Pest will report a failure.
./vendor/bin/pest --coverage --min=90Or, you can use the --exactly option to enforce that the coverage results match the specified value exactly.
./vendor/bin/pest --coverage --exactly=99.3Ignoring Code
If there are certain sections of your application that cannot be tested and should be excluded from code coverage analysis, you can use @codeCoverageIgnoreStart and @codeCoverageIgnoreEnd comments in your source code to achieve this.
// @codeCoverageIgnoreStart
function getUsers() {
//
}
// @codeCoverageIgnoreEndDifferent Formats
Pest supports various code coverage report formats:
--coverage-clover <file>: Save the code coverage report in Clover XML format to a specified file.--coverage-cobertura <file>: Save the code coverage report in Cobertura XML format to a specified file.--coverage-crap4j <file>: Save the code coverage report in Crap4J XML format to a specified file.--coverage-html <dir>: Save the code coverage report in HTML format to a specified directory.--coverage-php <file>: Serialize the code coverage data and save it to a specified file.--coverage-text <file>: Save the code coverage report in text format to a specified file. (Default: php://stdout)--coverage-xml <dir>: Save the code coverage report in XML format to a specified directory.
In this chapter, we discussed test coverage and its ability to aid in determining the percentage of your application that is actually tested. In the following chapter, we will dive into Pest's Type Coverage Plugin: Type Coverage
File: test-dependencies.md
title: Test Dependencies
Test Dependency
Sometimes, tests require certain preconditions or events to occur prior to their execution or else they will not succeed. For example, you may only be able to verify that users are able to modify their accounts if you have first verified that an account can be established.
To address this issue, Pest offers the depends() method, which allows a "Child" test to specify that it depends on one or more "Parent" tests.
test('parent', function () {
expect(true)->toBeTrue();
});
test('child', function () {
expect(false)->toBeFalse();
})->depends('parent');In this example, the child test will be triggered once the parent test has successfully completed.
If the parent test fails, the child test will be bypassed and an informative message will be displayed in your test results.
test('parent', function () {
expect(true)->toBeFalse();
});
test('child', function () {
expect(false)->toBeFalse();
})->depends('parent');The example above results in the following output:
It is important to remember that the it() function prefixes the test with "it" by default. Thus, when referencing the test name via the depends() method, you should include the "it " prefix.
it('is the parent', function () {
expect(true)->toBeTrue();
});
test('child', function () {
expect(false)->toBeFalse();
})->depends('it is the parent');Results is:
Parent tests can even provide return values that can be accessed as arguments in the child test.
test('parent', function () {
expect(true)->toBeTrue();
return 'from parent';
});
test('child', function ($parentValue) {
var_dump($parentValue); // from parent
expect($parentValue)->toBe('from parent');
})->depends('parent');It is also possible to add multiple dependencies to a test. However, all parent tests must pass, and the values returned by each test will be available as function parameters in the same order as the specified dependencies.
test('a', function () {
expect(true)->toBeTrue();
return 'a';
});
test('b', function () {
expect(true)->toBeTrue();
return 'b';
});
test('c', function () {
expect(true)->toBeTrue();
return 'c';
});
test('d', function ($testA, $testC, $testB) {
var_dump($testA); // a
var_dump($testB); // b
var_dump($testC); // c
})->depends('a', 'b', 'c');While test dependencies are uncommon, they can be useful for optimizing your tests and minimizing the need to recreate resources repeatedly. In the next chapter, we will explore how you can create plugins: Creating Plugins