PHPSpec – Defining custom inline matchers

I’ve been using PHPSpec for testing on my latest project. It’s the latest greatest thing to hit the PHP world don’t you know!

A side-effect of using the latest and greatest technology is that there tends to be a shortage of documentation, and more importantly a lack of stackoverflow solutions or general blog posts and tutorials. It is entirely possible that for someone more accustomed to testing, the patterns are very similar and therefore there isn’t the need, but I’m pretty much diving into TDD for the first time (finally!).

In case you’re in the same boat and having any difficulties I’m planning to write some posts explaining things that confused me at first and now I hopefully understand.

Matchers

PHPSpec comes with a wide array of matchers to compare your test against a result and ensure that your tested method is doing what it’s meant to be doing. Matchers like shouldBeEqual, shouldHaveCount, shouldHaveType are all clearly explained on the PHPSpec site and are quite simple to understand (and easy to write!).

One thing that didn’t instantly make sense to me though was the “Inline Matchers”, and now looking back on it, it is quite obvious how it works, but I think perhaps a few more examples of bespoke inline matchers might have helped clarify it for me.

Matching ‘Should be X or Y’

The first use-case I came up against was that my method was going to return an object (success!) or null. Fortunately my question was quickly answered on StackOverflow by Antonio Carlos Ribeiro and that answer suddenly made it clear to me how custom matchers really work.

Here’s the matcher I wrote which is referenced by the current spec.

public function getMatchers()
{
    return [
        'beObjectOrNull' => function($subject) {
            return is_object($subject) || $subject === null;
        },
    ]
}

So you create a getMatchers function and return an array of your custom matchers. This means you can define multiple custom matchers in one place and use them throughout the current spec. Presumably you can define your matchers in one location and use them throughout all specs but I haven’t needed to do this just yet – probably just change your spec class to extends MyProjectSpec, which in turn extends ObjectBehaviour.

Anyway, back to the matcher at hand. The ‘subject’ parameter is automatically set when you call your matcher.

The return statement here is quite simple if our subject is_object then true will be set, or if not then we check if the subject is null. If neither of those are true then the test will fail.

And here’s how to use the test:

function it_should_return_the_next_schedule()
{
    $this->getNextSchedule()->shouldBeObjectOrNull();
}

As in all uses of PHPSpec “$this” refers to the Class being tested, getNextSchedule() is the method in my Class under test, and I refer to my custom matcher with shouldBeObjectOrNull. Note that you need to prefix the inline matcher with ‘should’ (I think you can also use other words like ‘will’ and so on.

Hopefully that makes sense to you, if not, drop a comment. Also, if I’m doing something wrong – let me know!

Matching whether a Carbon DateTime object is correct

In this current project I’ve been having “fun” trying to handle complicated recurring schedule rules (shout out to simshaun for his fantastic Recurr package which definitely saved my sanity).

This time I needed a variety of different test cases for my “DatesCalculator” spec. Sometimes I just needed to know whether a DateTime object was being returned, other times I needed to check that when passed a certain set of variables, it would return either the right time or the right DateTime.

public function getMatchers()
{
    return [
        'beDateTimeObject' => function($subject) {
            return is_a($subject, 'DateTime');
        },
        'beSameDateTime' => function($subject, $value) {
            return $subject->format('Y-m-d H:i:s') === $value->format('Y-m-d H:i:s');
        },
        'haveSameTime' => function($subject, $value) {
            return $subject->format('H:i:s') == $value->format('H:i:s');
        },
    ];
}

In the first matcher I wanted to go a little further than shouldBeObject and make sure that the returned value from my method was a DateTime object.

The second matcher sees me checking that the actual DateTime is exactly what I expect it to be, with the third matcher being almost the same, but I only need to check that the times match. Note that in these last two matchers I’m passing another parameter in, so we need to make sure that parameter is present in the test.

Usage

function it_should_return_the_start_date_if_it_has_not_started()
{
    $now = new Carbon('-1 week');
    $scheduleStart = new Carbon('+1 week');
    $recurring_time_start = '17:00:00';

    $schedule = (object) array(
        'is_recurring'=> 0,
        'start' => $scheduleStart->format('Y-m-d H:i:s'),
    );
    $schedule2 = (object) array(
        'is_recurring'=> 1,
        'start' => $scheduleStart->format('Y-m-d 00:00:00'),
        'recurring_time_start' => '17:00:00',
    );

    $expectedResult1 = new Carbon($schedule->start);
    $expectedResult2 = $scheduleStart->format('Y-m-d ') . $schedule2->recurring_time_start;
    $expectedResult2 = new Carbon($expectedResult2);

    // The tests
    $this->hasScheduleStarted($schedule, $now)
         ->shouldBeSameDateTime($expectedResult1);
    $this->hasScheduleStarted($schedule2, $now)
         ->shouldBeSameDateTime($expectedResult2);
}

I won’t go into the nitty-gritty of how this works, but I’m (slightly hackily) mocking some basic objects that contain parameters of the sort I know will be passed to my method. I need to make sure that, given certain times and dates, my method will return the expected result.

In this test I’m actually testing two different conditions (hence the $expectedResult1 and $expectedResult2). Notice that the $value parameter (which is the second parameter in my inline matcher) is passed in as the ‘first’ parameter – the return from hasScheduleStarted() is automatically passed as the $subject.

I hope this is useful to someone. Feel free to ask any questions you might have, and as I say, I’m still learning this so please feel free to drop critical notes in the comments below!

Comments