unit-testinglaravelmockingphpunitprophecy

How to mock or stub with Laravel, PHPUnit, Prophecy, DI


Hello I'm trying to get my first mocking or stubbing test working with Prophecy. I've never used mocks and stubs or Mockery. I've done a fair number of unit tests where dependencies didn't really come into play, etc.

If I can get this one test working, I think it'll help me go a long with with further testing my code. Any help would be greatly appreciated!

First the error ...

Method `Double\App\Services\Maintenance\Flags\MaintenanceFlagsProvider\P1::findMostRecentByLabel()` is not defined.
/Library/WebServer/App/tests/unit/MaintenanceStatusTests.php:21

Here is the relevant portions of the test class ...

use App\Services\Maintenance\Logs\MaintenanceLogProvider;
use App\Services\Maintenance\Flags\MaintenanceFlagsProvider;
use App\Services\Maintenance\NextDueSchedules\NextDueScheduleProvider;
use App\Services\Components\Requirements\Status\RequirementStatusProvider;
use App\Services\Components\Requirements\Properties\ComponentRequirementPropertiesProvider;

class MaintenanceStatusTests extends TestCase
{
    /** @test */
    public function is_initial_returns_true()
    {
        $status = $this->buildStatus();

        $status->maintenance_flags_provider->shouldReceive('findMostRecentByLabel')->andReturn(new MaintenanceFlag(['value' => 1]));

        $this->assertTrue($status->isInitial());

    }

    private function buildStatus()
    {
        $maintenance_flags_provider = $this->prophesize(MaintenanceFlagsProvider::class);
        $maintenance_log_provider = $this->prophesize(MaintenanceLogProvider::class);
        $next_due_schedule_provider = $this->prophesize(NextDueScheduleProvider::class);
        $component_requirement_properties_provider = $this->prophesize(ComponentRequirementPropertiesProvider::class);

        return new RequirementStatusProvider($maintenance_flags_provider->reveal(), $maintenance_log_provider->reveal(), $next_due_schedule_provider->reveal(), $component_requirement_properties_provider->reveal());
    }
}

Here are the relevant portions of the class/method I'm attempting to test ...

namespace App\Services\Components\Requirements\Status;

use Carbon, StdClass;
use App\Services\Maintenance\Logs\MaintenanceLogProvider;
use App\Services\Maintenance\Flags\MaintenanceFlagsProvider;
use App\Services\Maintenance\NextDueSchedules\NextDueScheduleProvider;
use App\Services\Components\Requirements\Properties\ComponentRequirementPropertiesProvider;

class RequirementStatusProvider
{
    public $maintenance_flags_provider;
    public $maintenance_log_provider;
    public $next_due_schedule_provider;
    public $component_requirement_properties_provider;

    public $data;

    /**
     * @param MaintenanceFlagsProvider $maintenance_flags_provider
     * @param MaintenanceLogProvider $maintenance_log_provider
     * @param NextDueScheduleProvider $next_due_schedule_provider
     * @param ComponentRequirementPropertiesProvider $component_requirement_properties_provider
     */
    public function __construct(MaintenanceFlagsProvider $maintenance_flags_provider, MaintenanceLogProvider $maintenance_log_provider, NextDueScheduleProvider $next_due_schedule_provider, ComponentRequirementPropertiesProvider $component_requirement_properties_provider)
    {
        $this->data = new StdClass();

        $this->maintenance_flags_provider = $maintenance_flags_provider;
        $this->maintenance_log_provider = $maintenance_log_provider;
        $this->next_due_schedule_provider = $next_due_schedule_provider;
        $this->component_requirement_properties_provider = $component_requirement_properties_provider;
    }

// THIS IS WHAT I'M TRYING TO TEST ....
    public function isInitial()
    {
        $flag = $this->maintenance_flags_provider->findMostRecentByLabel('initial', $this->component->id, $this->requirement->id, $this->data->datetime);

        return (($flag && 1 == $flag->getAttribute('value')) || ! $this->data->pcw_logs[0]->event_at) ? 1 : 0;
    }
}

I have also revised the test in the following way ...

/** @test */
public function is_initial_returns_true()
{
    $maintenance_log_provider = $this->prophesize(MaintenanceLogProvider::class);
    $maintenance_flags_provider = $this->prophesize(MaintenanceFlagsProvider::class);
    $next_due_schedule_provider = $this->prophesize(NextDueScheduleProvider::class);
    $component_requirement_properties_provider = $this->prophesize(ComponentRequirementPropertiesProvider::class);

    $maintenance_flags_provider->findMostRecentByLabel('initial', 1, 1, Carbon::now())->willReturn(new MaintenanceFlag(['value' => 1]));

    $status = new RequirementStatusProvider($maintenance_flags_provider->reveal(), $maintenance_log_provider->reveal(), $next_due_schedule_provider->reveal(), $component_requirement_properties_provider->reveal());

    $this->assertTrue($status->isInitial());
}

Even if I omit the ->willReturn portion I still get the exact same error.

I don't want isInitial to call the actual MaintenanceFlagsProvider class, I want the test to fake that call but set the $flag variable to whatever I want via the test (if that makes sense).


Solution

  • Part of the issue was with my provider which was forwarding undefined method calls to the corresponding repository class. I was able to make Prophecy happy by adding a @method signature for the missing method to the provider class

    /**
     * @method findMostRecentByLabel(array $args = [])
     */
    class MaintenanceFlagsProvider
    {
        ...
    }
    

    The working test ...

    /** @test */
    public function is_initial_returns_true()
    {
        $date = Carbon::now();
    
        $maintenance_log_provider = $this->prophesize(MaintenanceLogProvider::class);
        $maintenance_flags_provider = $this->prophesize(MaintenanceFlagsProvider::class);
        $next_due_schedule_provider = $this->prophesize(NextDueScheduleProvider::class);
        $component_requirement_properties_provider = $this->prophesize(ComponentRequirementPropertiesProvider::class);
    
        $maintenance_flags_provider->findMostRecentByLabel('initial', 1, 1, $date)->shouldBeCalled()->willReturn(new MaintenanceFlag(['value' => 1]));
    
        $status = new RequirementStatusProvider($maintenance_flags_provider->reveal(), $maintenance_log_provider->reveal(), $next_due_schedule_provider->reveal(), $component_requirement_properties_provider->reveal());
    
        $status->component = (new Component())->forceFill(['id' => 1]);
        $status->requirement = (new MaintenanceRequirement())->forceFill(['id' => 1]);
        $status->data->datetime = $date;
    
        $this->assertTrue($status->isInitial());
    }