Buy Access to Course
06.

Coding up the Custom Matcher

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

According 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!

Peeking at the Core Matchers

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.

30 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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.

30 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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

Implementing supports()

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.

30 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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.

33 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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

Implementing Matches

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.

33 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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].

41 lines | spec/Matcher/BeGreaterMatcher.php
// ... lines 1 - 9
protected function matches($subject, array $arguments): bool
{
return $subject > $arguments[0];
}
// ... lines 14 - 41

Implementing the Failure Exception Methods

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.

41 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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.

41 lines | spec/Matcher/BeGreaterMatcher.php
// ... 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.