1

I'm building a Laravel 5 application at the moment and have gotten myself confused about how to mock things in PhpSpec.

I'm building a schedule times validator that requires the intended schedule to be checked against all current schedules and see if there's any overlap (events are not allowed to overlap).

I need to pull in the schedules in question so I can test against them. At the moment it's a very basic whereBetween query, but it's going to get a lot more complicated as there'll be recurring schedules to check against as well.

So here's my stripped down class. I really just want to test the doesNotOverlap function.

use App\Schedule;

class ScheduleTimesValidator
{
    protected $schedule;

    public function __construct(Schedule $schedule)
    {
        $this->schedule = $schedule;
    }

    public function doesNotOverlap($slug, $intended)
    {
        $schedules = $this->getSchedulesBetween($slug, $intended);
        if(empty($schedules)) return true;

        return false;
    }

    protected function getSchedulesBetween($slug, $intended)
    {
        // Casting to array to make testing a little easier
        return $this->schedule->whereIsRecurring(false)
                                ->ofChurch($slug)
                                ->whereBetween('start', [$intended['start'], $intended['end']])
                                ->get()->toArray();
    }

and here's my Spec

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ScheduleTimesValidatorSpec extends ObjectBehavior
{
    protected $validIntended = [
        'start' => '2015-12-01 12:00:00',
        'end' => '2015-12-01 13:00:00'
    ];

    protected $churchNonRecurringSchedules = [
        ['start' => '2014-11-20 13:00:00', 'end' => '2014-11-21 14:00:00'],
        ['start' => '2014-11-23 10:36:07', 'end' => '2014-11-23 11:36:07'],
    ];

    function let($schedule)
    {
        $schedule->beADoubleOf('App\Schedule');
        $this->beConstructedWith($schedule);
    }


    function it_is_initializable()
    {
        $this->shouldHaveType('App\Validation\ScheduleTimesValidator');
    }

    function it_should_return_true_if_it_does_not_overlap($schedule)
    {
        // $schedule->any()->willReturn([]);
        // $schedule->whereIsRecurring()->shouldBeCalled();
        // $schedule->whereIsRecurring(false)->ofChurch()->whereBetween()->get()->toArray()->willReturn([]);
        // $schedule->willReturn([]);
        // $this->getSchedulesBetween('slug', $this->validIntended)->willReturn([]);
        $this->doesNotOverlap('slug', $this->validIntended)->shouldReturn(true);
    }

    // Tear Down
    function letgo() {}

}

If I run it like that I get:

! it should return true if it does not overlap
    method 'Double\App\Schedule\P8::whereIsRecurring()' not found.

I tried (as you can see) various commented out things to mock what $schedule will return, but that doesn't seem to work.

So I guess I want to mock the protected getSchedulesBetween method in the class, but doing things like $this->getSchedulesBetween($arg, $arg)->willReturn(blah) doesn't work.

Do I need to pull getSchedulesBetween() out of the class and move it into another class and then mock that? Or do I need to push $this->schedule->blah into the doestNotOverlap method so I can mock what $schedule will return?

I don't want to actually test the App\Schedule Laravel Model - I just want to mock what it's returning and will be hardcoding a variety of queries that will be run to get the different model results.

End of a long day here so brain a little zonked.

Update 2014-10-23

So I created a scope on my Schedule model

public function scopeSchedulesBetween($query, $slug, $intended)
{
    return $query->whereIsRecurring(false)
                    ->ofChurch($slug)
                    ->whereBetween('start', [$intended['start'], $intended['end']]);
}

Then created a new App\Helpers\ScheduleQueryHelper which instantiated App\Schedule as a variable and added this method:

public function getSchedulesBetween($slug, $intended)
{
    return $this->schedule->schedulesBetween($slug, $intended)->get()->toArray();
}

Then updated my spec to do

function let($scheduleQueryHelper)
{
    $scheduleQueryHelper->beADoubleOf('App\Helpers\ScheduleQueryHelper');
    $this->beConstructedWith($scheduleQueryHelper);
}

function it_should_return_true_if_it_does_not_overlap($scheduleQueryHelper)
{
    $scheduleQueryHelper->getSchedulesBetween('slug', $this->validIntended)->willReturn([]);
    $this->doesNotOverlap('slug', $this->validIntended)->shouldReturn(true);
}

And back in my ScheduleTimesValidator class did

public function doesNotOverlap($slug, $intended)
{
    $schedules = $this->scheduleQueryHelper->getSchedulesBetween($slug, $intended);

    if(empty($schedules)) {
        return true;
    }

    return false;
}

And now PhpSpec is mocking that other class ok. However this seems like a very roundabout way to be doing things.

alexleonard
  • 1,314
  • 3
  • 21
  • 37
  • Have you tried to `let(App\Schedule $schedule)` and remove `beADoubleOf`. I'm not sure if it will make any difference but I don't see why it's not finding that getSchedulesBetween() method in the mock object, because `$this->getSchedulesBetween('slug', $this->validIntended)->willReturn([]);` should be enough for `doesNotOverlap` – Antonio Carlos Ribeiro Oct 22 '14 at 17:44
  • Hmm, that didn't seem to make a difference. I tried reinstating `$this->getSchedulesBetween('slug', $this->validIntended)->willReturn([]);` and first got `method App\Validation\ScheduleTimesValidator::getSchedulesBetween not visible` so I changed the getSchedulesBetween back to public and once again got `method 'Double\App\Schedule\P8::whereIsRecurring()' not found`. Overnight I was thinking I should really make that method a scope, so I'm going to try that out and see what happens. – alexleonard Oct 23 '14 at 08:12
  • So I created a scope on the schedule model which does all of whereIsRecurring, whereBetween, and ofChurch; and then moved that directly into my doesNotOverlap method `$schedules = $this->schedule->schedulesBetween($slug, $intended)->get();`. Then in my spec I tried `$schedule->schedulesBetween('slug', $this->validIntended)->willReturn((object)[]);` but still get `method 'Double\App\Schedule\P8::schedulesBetween()' is not defined.`. All I can think of at the moment is creating another class which does the Eloquent queries and mocking that, but that seems like excess classes..! – alexleonard Oct 23 '14 at 08:34
  • I wonder is this relevant? https://github.com/phpspec/phpspec/issues/149 – alexleonard Oct 23 '14 at 08:35
  • It also feels like a similar problem I was experiencing here: http://stackoverflow.com/questions/23002474/phpspec-and-laravel-how-to-handle-double-method-not-found-issues – alexleonard Oct 23 '14 at 08:35
  • @AntonioCarlosRibeiro I got it working by creating another in-between class, but I'm pretty sure I'm doing something wrong as that seems to defeat the purpose of mocking what a class willReturn. I have a feeling I'm totally misunderstanding something :D – alexleonard Oct 23 '14 at 08:56
  • Yeah, seems like a lot of trouble to do something that should be simple. You could try to talk to people on FreeNode's #phpspec. Or even open an issue in https://github.com/phpspec/prophecy, because this is a Prophecy matter. The latter will probably be answered faster. :) – Antonio Carlos Ribeiro Oct 23 '14 at 10:37
  • I'm getting the feeling I need to go down a repository/interface approach to make all of this testable. – alexleonard Oct 23 '14 at 14:43
  • I know this post is old, but for reference, there is a great article by Martin Fowler that describes the difference between a stub and a mock. http://martinfowler.com/articles/mocksArentStubs.html – Andres Zapata Sep 10 '16 at 16:21

0 Answers0