Writing tests is sometimes a hard an time consuming task. Especially if you have functionality that cannot be easily tested with your testing framework. phpspec provides an awesome set of so called matchers. But sometimes the predefined matchers are not useful or suitable for implementing a certain test case.
In this article I will use a very trivial example for a custom matcher. Let’s asume you have some sort of random mechanism that outputs an integer in the range [1,5] or a random string from a list. It would be cool to have a matcher to do a call like $this->something()->shouldBeAnyOf(1,2,3,4,5)
or $this-something()->shouldBeAnyOf('string1', 'string2', 'string3')
.
The first approach is a very simple one. In a phpspec specification file you have the possibility to define a method called getMatchers()
.This method will return an array of custom matchers for this specification file. The key of each array entry is the matcher’s method signature name and the value is a callable that will get executed when the matcher is used. This callable gets at least one parameter, the $subject
, this is the value returned from the method call. If your matcher uses input parameters, these will be added after the $subject
parameter.
<?php
public function getMatchers()
{
return [
"beAnyOf" => function ($subject, ...$list) {
if (!in_array($subject, $list)) {
throw new FailureException(
sprintf(
'the return value "%s" is not contained in "%s"',
$subject,
implode(', ', $list);
)
);
}
return true;
})
];
}
Ok, now that we have implemented our inline matcher we can use it by calling:
<?php
$this->method()->shouldBeAnyOf(1, 2, 3);
If $this->method()
returns the value 1, 2 or 3 the test will pass, otherwise a error message like the return value "4" is not contained in "1, 2, 3"
will be printed.
This approach is valid for single-use matchers but when it comes to reusable matchers another approach is used.
To be able to share matchers across specification files and projects you need to extract the matcher logic into a phpspec extension. Writing a phpspec extension is dead simple but not well documented. I will demonstrate the plugin development with the matcher example from above.
To implement a plugin with a custom matcher you basically need two files. One for bootstrapping the extension and a second for the matcher implementation.
The entry point of a phpspec extension is a class that implements the PhpSpec\Extension
interface. This interface defines the method load
which is used for bootstrapping the extension. The method accepts two parameters, the ServiceContainer
and an array of additional parameters.
For our simple example it will be enough to use the ServiceContainer
for registering our matcher. This can be done with the following code.
<?php
class CustomExtension implements PhpSpec\Extension
{
public function load(ServiceContainer $container, array $params)
{
$container->define(
'custom.matchers.be_any_of',
function ($c) {
return new BeAnyOfMatcher();
},
['matchers']
);
}
}
The matcher implementation, in our case BeAnyOfMatcher
, must implement the PhpSpec\Matcher
interface. This interface defines four methods.
The method accepts 3 arguments, the matcher name, the subject (the return value of the method call) and an array of arguments. The purpose of this message is to check if the given parameters are suitable for the matcher. In our example we want the matcher name to be equal to beAnyOf
and there should be at least one matcher argument.
<?php
public function supports($name, $subject, array $arguments)
{
return $name === 'beAnyOf' && count($arguments) > 0;
}
This will be the method that gets called when shouldBeAnyOf
is used in a test case. The method receives the same three arguments as the supports
method. The method should check the positive case (the $subject
should be contained in the $arguments
array) and if the positive case is not matched, it should throw an exception.
<?php
public function positiveMatch($name, $subject, array $arguments)
{
if (!in_array($subject, $arguments)) {
throw new FailureException(
sprintf(
'the return value "%s" should be any of "%s"',
$subject,
implode(', ', $arguments)
)
);
}
}
This will be the method that gets called when shouldNotBeAnyOf
is used. The method receives the same parameters as the two methods before. The method should evaluate the negative case ($subject
is not contained in the $arguments
array) and if it is not matched should throw an exception.
<?php
public function negativeMatch($name, $subject, array $arguments)
{
if (in_array($subject, $arguments)) {
throw new FailureException(
sprintf(
'the return value "%s" should not be any of "%s"',
$subject,
implode(', ', $arguments)
)
);
}
}
This method returns an integer priority value that is used for some sort of matcher ranking. E.g. Matcher A should be evaluated before Matcher B –> Priority of A is greater than of B.
To be able to use the extension above we need to instruct phpspec to load the main entry point of our extension. To do so you simply add the following section to your phpspec.yml
.
extensions:
CustomNamespace\CustomExtension: ~
As you can see writing a custom matcher for phpspec is dead simple. You put the code either inline in the specification file or extract it into its own package. If recently started to collect a (hopefully) useful set of custom matchers in the phpspec-matchers package. This package will give you an idea about how to glue together the things described above.
If you have any comments on the package or find any bugs or enhancements, please feel free to file an issue or send a pull request.