Higher Order Expectations in Pest
Published 11/06/2021 | Last updated 17/12/2021 | 967 viewsJoin 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