Higher Order Expectations in Pest

Last updated 11/06/2021

Join me, Sam, to dive into a new feature to prove once again that Pest is definitely not a pest when testing...

Before we start, I just want to give a big shout out to Luke for allowing me a guest slot for this article. Thanks man

Hi, I'm Sam! Yes really, I'm not Luke. Sorry to disappoint... But hopefully you'll stick with me as we talk about a new feature for Pest...

Pest

Pest is a fantastic tool to use when testing your code with, amongst many other features, its expressive expectations API wrapping PHPUnit's assertions. As just a peek of what's possible, we can write the following expectations:

expect($user->first_name)->toEqual('Sam')
expect($user->last_name)->toEqual('Rowden')
expect($user->posts)->toHaveCount(2)->each->toBeInstanceOf(Post::class)

Which is super tidy and clean.

And now, we can take our testing with Pest one step further...

Higher Order Expectations

Introduced in Pest 1.4 (and 1.6 of the expectations plugin), we now have Higher Order Expectations in our toolkit. These allow us to access properties and methods from the expectation value itself, rather than accessing them ahead of time.

Enough talking, let's see it in action! Taking our test from above, we can now refactor it to:

expect($user)
    ->first_name->toEqual('Sam')
    ->last_name->toEqual('Rowden')
    ->posts->toHaveCount(2)->each->toBeInstanceOf(Post::class)

Pest looks after accessing the properties on the object, so now our tests can be just that little bit more clean and expressive.

We mentioned that it's possible to access methods too - and we can even pass in parameters:

expect($user)
    ->fullName()->toEqual('Sam Rowden')
    ->isAuthorOf($post)->toBeTrue

As well as objects, we can access arrays of data just the same:

expect(['name' => 'Sam', 'posts' => [...]])
    ->name->toEqual('Sam')
    ->posts->not->toBeEmpty

Note that we can use ->syntax rather than having to use array['syntax']

One More Thing

As higher order expectations are created automatically from expectations, it means we can do some quite powerful things when used in conjunction with each() and sequence(). When we pass a callable to each or sequence, an individual assertion is run against each item in the iterable. Which means we can now do something like this:

expect($user)
    ->posts
    ->toHaveCount(2)
    ->each(fn ($post) => $post->is_published->toBeTrue)

Notice we were able to dig deeper into each iterable and access the is_published property on each one. And with sequence(), we can go further:

expect($user)
    ->posts
    ->toHaveCount(2)
    ->sequence(
        fn ($post) => $post->title->toEqual('Hello World'),
        fn ($post) => $post->title->toEqual('Simply the Pest'),
    )

Top Tips

  • Any methods and properties that Pest owns will be called from Pest, not the value. For example, imagine for some reason your object had a sequence method:
expect($service)->sequence()->... // This accesses Pest's sequence, not your service
  • Higher order expectations are currently able to go 1 level deep from your value:
expect($user)->company->name->... // This is not possible

Conclusion

I hope higher order expectations are as fun for you to use as they were for me to write and add to Pest. You might not want to use them everywhere, but I know there's already some tests I can't wait to refactor using them.

It'd be great to hear what you think of this feature, you can reach me at @nedwors.

Again, a final thanks to Luke - @LukeDowning19 - for letting me invade his blog. And thanks for being a great mentor of all things coding.

And of course, thanks to the guys at Pest for the amazing library, and being super supportive through this whole thing.

Sam