laravelunit-testingphpunitmockery

Why is my filtered relations collection empty despite mocking a valid HasMany relation?


I'm writing a unit test to filter out invalid relations from a model. The goal is to only keep relations that actually exist in the model. However, my test is failing because the filtered relations collection is coming back empty, even though I've mocked one of the relations validRelation to return a valid HasMany relationship.

Additional Context:

What could be causing this behavior, and how can I properly test that valid relations are not filtered out?

protected function filterRestrictedRelations(): void
{
    $this->restrictedRelations = $this->restrictedRelations->filter(function ($relation) {
        $exists = method_exists($this->model, $relation);
        dump("Checking relation: $relation, exists: " . ($exists ? 'true' : 'false'));
        return $exists;
    });
}

enter image description here

Here's the relevant part of my test:

public function setUp(): void
{
    parent::setUp();
    $this->model = \Mockery::mock(AnAbstractClass::class)->makePartial();
    $this->modelRelation   = \Mockery::mock(new ModelRelation($this->model));
    $this->reflectionClass = new ReflectionClass($this->modelRelation);
}


public function it_filters_out_relations_that_do_not_exist_in_model(): void
{
    $relations = ['validRelation', 'invalidRelation1', 'invalidRelation2'];

    // Mocking model to have the 'validRelation' method returning a HasMany relation
    $this->model->shouldReceive('validRelation')
                ->andReturn(\Mockery::mock(HasMany::class));

    $this->modelRelation = new ModelRelation($this->model);
    $this->modelRelation->setRestrictedRelations($relations);

    $this->reflectionClass = new ReflectionClass($this->modelRelation);

    // Invoke the filterRestrictedRelations method
    $this->reflectionClass->getMethod('filterRestrictedRelations')
                          ->invoke($this->modelRelation);

    $filteredRelations = $this->reflectionClass->getMethod('getRestrictedRelations')->invoke($this->modelRelation);

    // Check that invalidRelation1 remains, while others are filtered out
    $this->assertEquals(['validRelation'], $filteredRelations->all());
}

Solution

  • When using Mockery to mock an existing class, the method_exists() function will return true for any method that actually exists on the original class, but it will return false for methods that don't exist, even if you use shouldReceive().

    This happens because shouldReceive() does not create a method in the conventional way; rather, it leverages the __call() magic method for method overloading.

    Mockery offers a method to verify if a mock is expecting a specific method call. Rather than using method_exists(), you can determine if a mock has any expectations set for that particular method.

    protected function filterRestrictedRelations(bool $mocking = false): void
    {
        $this->restrictedRelations = $this->restrictedRelations->filter(function ($relation) {
            return method_exists($this->model, $relation) || ($mocking && $this->model->mockery_getExpectationsFor($relation));
        });
    }