// docs / custom-expectations

Custom Expectations

Pest's expectation API is powerful by default. However, 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 convenient.

Custom expectations are usually defined in the tests/Pest.php file, but you may 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 may create a custom expectation called toBeWithinRange():

1// Pest.php or Expectations.php
2expect()->extend('toBeWithinRange', function (int $min, int $max) {
3 return $this->toBeGreaterThanOrEqual($min)
4 ->toBeLessThanOrEqual($max);
5});
6 
7// Tests/Unit/ExampleTest.php
8test('numeric ranges', function () {
9 expect(100)->toBeWithinRange(90, 110);
10});

While users typically rely on 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 logic. In such cases, you may access the value that was passed to expect($value) via the $this->value property:

1expect()->extend('toBeWithinRange', function (int $min, int $max) {
2 echo $this->value; // 100
3});

Of course, you may 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:

1// Pest.php or Expectations.php
2expect()->extend('toBeWithinRange', function (int $min, int $max) {
3 // Assertions based on `$this->value` and the given arguments...
4 
5 return $this; // Return this, so other expectations may chain onto this one...
6});
7 
8// Tests/Unit/ExampleTest.php
9test('numeric ranges', function () {
10 expect(100)
11 ->toBeInt()
12 ->toBeWithinRange(90, 110)
13 ->to...
14});

Sometimes you may need to trigger a test failure within your custom expectation. To do so, use the test() method in combination with the fail() method:

1// Pest.php or Expectations.php
2expect()->extend('toBeDivisibleBy', function (int $divisor) {
3 if ($divisor === 0) {
4 test()->fail('The divisor cannot be 0.');
5 }
6 
7 return expect($this->value % $divisor)->toBe(0);
8});
9 
10// Tests/Unit/ExampleTest.php
11test('numeral division', function () {
12 expect(10)->toBeDivisibleBy(2); // Pass
13 expect(10)->toBeDivisibleBy(0); // Fail "The divisor cannot be 0."
14});

Intercept Expectations

Although it is considered an advanced practice, you may override existing expectations with your own implementation via the intercept() method. When you use this method, the existing expectation will be fully substituted if the expectation value is of the specified type. For example, you may replace the toBe() expectation to check whether two objects of the Illuminate\Database\Eloquent\Model type have the same id:

1use Illuminate\Database\Eloquent\Model;
2use App\Models\User;
3 
4// tests/Pest.php or tests/Expectations.php
5expect()->intercept('toBe', Model::class, function(Model $expected) {
6 expect($this->value->id)->toBe($expected->id);
7});
8 
9// tests/Feature/ExampleTest.php
10test('models', function () {
11 $userA = User::find(1);
12 $userB = User::find(1);
13 
14 expect($userA)->toBe($userB);
15});

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:

1expect()->intercept('toBe', fn (mixed $value) => is_string($value), function (string $expected, bool $ignoreCase = false) {
2 if ($ignoreCase) {
3 assertEqualsIgnoringCase($expected, $this->value);
4 } else {
5 assertSame($expected, $this->value);
6 }
7});

Pipe Expectations

Sometimes you may wish to run one of Pest's built-in expectations, yet include customized logic under certain conditions. In these cases, you may 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:

1use Illuminate\Database\Eloquent\Model;
2use App\Models\User;
3 
4expect()->pipe('toBe', function (Closure $next, mixed $expected) {
5 if ($this->value instanceof Model) {
6 return expect($this->value->id)->toBe($expected->id);
7 }
8 
9 return $next(); // Run the original, built-in expectation...
10});

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. Next, let's explore how to isolate the code under test by mocking its dependencies: Mocking