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.

1it('works', function () {
2 $this->get('/')
3 ->assertStatus(200);
4});

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.

1it('works')
2 ->get('/')
3 ->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.

1it('has a name', function () {
2 $user = User::create([
3 'name' => 'Nuno Maduro',
4 ]);
5 
6 expect($user->name)->toBe('Nuno Maduro');
7});

If your test contains only one expectation, we can simplify it using high-order testing.

1it('has a name')
2 ->expect(fn () => User::create(['name' => 'Nuno Maduro'])->name)
3 ->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.

1it('creates admins')
2 ->defer(fn () => $this->artisan('user:create --admin'))
3 ->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.

1beforeEach(function () {
2 $this->withoutMiddleware();
3});
4 
5// Can be rewritten as...
6beforeEach()->withoutMiddleware();

When using higher order testing, dataset values are passed to the expect() and defer() closures for convenience.

1it('validates emails')
2 ->with(['taylor@laravel.com', 'enunomaduro@gmail.com'])
3 ->expect(fn (string $email) => Validator::isValid($email))
4 ->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:

1expect($user->name)->toBe('Nuno');
2expect($user->surname)->toBe('Maduro');
3expect($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.

1expect($user)
2 ->name->toBe('Nuno')
3 ->surname->toBe('Maduro')
4 ->addTitle('Mr.')->toBe('Mr. Nuno Maduro');

When working with arrays, you may also access the $value array keys and perform expectations on them.

1expect(['name' => 'Nuno', 'projects' => ['Pest', 'OpenAI', 'Laravel Zero']])
2 ->name->toBe('Nuno')
3 ->projects->toHaveCount(3)
4 ->each->toBeString();
5 
6expect(['Dan', 'Luke', 'Nuno'])
7 ->{0}->toBe('Dan');

Higher Order Expectations can be used with all Expectations, and you may even create further Higher Order Expectations within closures.

1expect(['name' => 'Nuno', 'projects' => ['Pest', 'OpenAI', 'Laravel Zero']])
2 ->name->toBe('Nuno')
3 ->projects->toHaveCount(3)
4 ->sequence(
5 fn ($project) => $project->toBe('Pest'),
6 fn ($project) => $project->toBe('OpenAI'),
7 fn ($project) => $project->toBe('Laravel Zero'),
8 );

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.

1expect($user)
2->name->toBe('Nuno')
3->email->toBe('enunomaduro@gmail.com')
4->address()->scoped(fn ($address) => $address
5 ->line1->toBe('1 Pest Street')
6 ->city->toBe('Lisbon')
7 ->country->toBe('Portugal')
8);

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