Handling custom constraints in Pest

8th Jan 2021

Pest supports two methods of performing custom constraints. The more expressive and extensible PHPUnit-style constraints, and the cleaner Pest-style syntax of custom expectations.

Pest expectations

The simpler Pest syntax for handling custom constraints are "custom expectations". These allow you to extend the core expect() method with whatever expectations are necessary.

For example, if you wanted to check a value is within a range of two integers, you could add the following expectation in your tests/Pest.php.

expect()->extend('toBeWithinRange', function (int $min, int $max) {
   return $this->toBeGreaterThanOrEqual($min)
               ->toBeLessThanOrEqual($max);
});

Which can then be used as the following in your tests

expect(100)->toBeWithinRange(90, 110);

These expectations can also be distributed as part of your own packages. For example, if you were handling JSON files a lot, you could create a package for all of the common actions around validating JSON.

PHPUnit constraints

The alternative method is using PHPUnit constraints. These are a rarely discussed type of class that are available using the toMatchConstraint() expectation in Pest.

As noted in the Pest documentation, custom constraints should extend PHPUnit\Framework\Constraint\Constraint, and provide a matches() and toString() method, and optionally override the evaluate() method.

For example, to implement the same code as above, we can create a new class as follows:

class WithinRange extends \PHPUnit\Framework\Constraint\Constraint
{
    public function __construct(int $min, int $max)
    {
        $this->min = $min;
        $this->max = $max;
    }

    public function toString(): string
    {
        return sprintf(
            'is within a range of %s and %s',
            $this->exporter()->export($this->min),
            $this->exporter()->export($this->max)
        );
    }

    public function matches($other): bool
    {
        return $other > $this->min && $other < $this->max;
    }
}

These can then be used as below in your tests:

expect(100)->toMatchConstraint(new WithinRange(90, 110));

You can also combine both methods to have a constraint inside a simple Pest expectation:

expect()->extend('toBeWithinRange', function (int $min, int $max) {
    return $this->toMatchConstraint(new WithinRange($min, $max));
});

Now that the Pest Expectations plugin has been extracted from Pest to work in PHPUnit, either method is possible to use. However, the custom constraints can be used with much older PHPUnit versions as well as the newest releases that Pest requires.