If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAccording to this friendly error, a custom matcher has one important rule: it must implement this Matcher
interface. Cool! To see what these classes normally look like, let's cheat and dig into some of the core matchers themselves!
Open vendor/phpspec/phpspec/src/PhpSpec
and look in the Matcher/
directory. Say hello to all the core matchers. Check out ThrowMatcher
- a matcher we'll use later to help us test exceptions. Yep! It implements that interface. Let's also look at one we're already using a lot: IdentityMatcher
.
Oh - instead of implementing the Matcher
interface directly, this extends a BasicMatcher
class, which implements that interface, but handles a lot of the low-level work. Most of the time, you'll probably want to extend this class - it just makes life easier.
Let's go! I'll close a few other files. Then, make our BeGreaterMatcher
extend BasicMatcher
. I'm also going to mark the class as final
. There's no reason for that - it's just a general nice practice to mark a class as final unless you intend for it to be sub-classed. Though, marking a class as final can cause issues if you need to mock it. Either way - this is not important to get this all working.
... lines 1 - 7 | |
final class BeGreaterMatcher extends BasicMatcher | |
{ | |
... lines 10 - 28 | |
} |
Next, go to the Code -> Generate
menu or Command+N on a Mac, select "Implement Methods" and implement all four methods that we need.
... lines 1 - 9 | |
protected function matches($subject, array $arguments): bool | |
{ | |
// TODO: Implement matches() method. | |
} | |
protected function getFailureException(string $name, $subject, array $arguments): FailureException | |
{ | |
// TODO: Implement getFailureException() method. | |
} | |
protected function getNegativeFailureException(string $name, $subject, array $arguments): FailureException | |
{ | |
// TODO: Implement getNegativeFailureException() method. | |
} | |
public function supports(string $name, $subject, array $arguments): bool | |
{ | |
// TODO: Implement supports() method. | |
} | |
... lines 29 - 30 |
The first important method is supports()
. Let's var_dump()
the arguments to see what we're working with: $name
, $subject
and $arguments
. Whenever any matcher is used inside of a spec class, phpspec loops over all of the matcher classes and calls supports()
to figure out which one to use.
... lines 1 - 24 | |
public function supports(string $name, $subject, array $arguments): bool | |
{ | |
var_dump($name, $subject, $arguments); | |
} | |
... lines 29 - 30 |
Let's see what happens! Run phpspec:
./vendor/bin/phpspec run
Yep! It's dumping out every time any matcher is used - once for haveType
, returnZero
and down here for beGreaterThan
. That's the $name
argument. The $subject
is 15
because, in our spec class, getLength()
returns 15 and we're calling the matcher on that value. Finally, $arguments
is an array
with just one entry: 12 - because we're passing 12 as the one argument to the shouldBeGreaterThan()
matcher.
The supports()
method really only needs to check to make sure that the $name
is equal to beGreaterThan
: that's enough to tell phpspec that we handle that. But, a lot of times, these are written to be a bit more flexible. For example, you could use in_array()
to check that $name
is one of beGreater
or beGreaterThan
. Then, if you want, you can even make sure the types of the subject and arguments are what we expect. We'll say that this matcher should only be used if is_numeric($subject)
and if count($arguments)
is exactly one and if that argument is also numeric and if it's Halloween after midnight. Kidding.
... lines 1 - 24 | |
public function supports(string $name, $subject, array $arguments): bool | |
{ | |
return in_array($name, ['beGreater', 'beGreaterThan']) | |
&& is_numeric($subject) | |
&& count($arguments) === 1 | |
&& is_numeric($arguments[0]); | |
} | |
... lines 32 - 33 |
So, when supports()
returns true
, phpspec will then call the match()
function on top. Our job here is to return true
if everything looks good, or false otherwise - just like the inline matcher. Let's var_dump
the $subject
and $arguments
one more time with a die
statement - to make sure it's called like we expect.
... lines 1 - 9 | |
protected function matches($subject, array $arguments): bool | |
{ | |
var_dump($subject, $arguments);die; | |
} | |
... lines 14 - 33 |
Move over and try phpspec again:
./vendor/bin/phpspec run
Yes! We are called with 15 as the $subject
and the same $arguments
array with 12 inside. Because we know there will always be exactly one argument, we can finish this method return $subject > $arguments[0]
.
... lines 1 - 9 | |
protected function matches($subject, array $arguments): bool | |
{ | |
return $subject > $arguments[0]; | |
} | |
... lines 14 - 41 |
Done! If the matcher returns false, phpspec will either call getFailureException()
or getNegativeFailureException()
- the difference is if we're using shouldBeGreaterThan()
or shouldNotBeGreaterThan()
. Our job there is to return that same FailureException
- I'll paste one in with a good message. This is the same type of object that we're throwing from our inline matcher.
... lines 1 - 14 | |
protected function getFailureException(string $name, $subject, array $arguments): FailureException | |
{ | |
return new FailureException(sprintf( | |
'Expected %d to be greater than %d', | |
$subject, | |
$arguments[0] | |
)); | |
} | |
... lines 23 - 41 |
For the other method, copy this, paste, and just tweak the language a little bit.
... lines 1 - 23 | |
protected function getNegativeFailureException(string $name, $subject, array $arguments): FailureException | |
{ | |
return new FailureException(sprintf( | |
'Expected %d to not be greater than %d', | |
$subject, | |
$arguments[0] | |
)); | |
} | |
... lines 32 - 41 |
We now have a fully-functional custom matcher that allows us to use this new, natural, language inside of any spec file. Well... assuming it works - try it out:
./vendor/bin/phpspec run
All green! Next: it's time to talk about how phpspec fits into the entire world of testing. For example, there are functional tests, integration tests, and unit tests... and multiple tools like PHPUnit and Behat. Let's dive into this big mess of tools and buzzwords.
"Houston: no signs of life"
Start the conversation!