Waqleh
Waqleh

Reputation: 10161

How to Mock Objects in PHP to Work Correctly with Null Coalescing Operator (??)?

I'm trying to write a PHPUnit test using Mockery, and I keep running into issues when using the null coalescing operator (??) with mocked properties. Specifically, ?? seems to always return null, which causes the ?? operator to fall back to the default value, even though the property is mocked correctly and returns a value. Here’s a simplified version of my setup:

public function testExample()
{
    $model = Mockery::mock(Model::class)->shouldIgnoreMissing();

    // Mocking a property
    $model->shouldReceive('getAttribute')
        ->with('title')
        ->andReturn('Test Page');

    $model->title = 'Test Page'; // I also tried this

    $result = $this->doSomething($model);

    $this->assertEquals('/test-page', $result);
}

public function doSomething($model): string
{
    print_r([$model->title, $model->title ?? 'default']);
    ...
    ...
}

Output:

Array
(
    [0] => Test Page
    [1] => default
}

When I use $model->title directly, it works fine and returns the mocked value ('Test Page'). However, when I try to use $model->title ?? 'default', the fallback ('default') is always returned, as if the title property doesn't exist or is null.

Is there a way to make the null coalescing operator (??) work reliably with Mockery mocks in PHP?

Stack: PHP 8.2, PHPUnit 10.5 with Laravel 11.33.2

Note: I'm not trying to test the model itself or any part of Laravel's framework. My goal is simply to make the model return a specific value in a practical way without setting up factories its relations.

Upvotes: 0

Views: 74

Answers (2)

Waqleh
Waqleh

Reputation: 10161

I was able to solve the issue by using a factory as suggested by Flame, rather than solely relying on mocking. Here's how I implemented it:

Instead of this approach:

$model = Mockery::mock(Model::class)->shouldIgnoreMissing();
$model->shouldReceive('getAttribute')
      ->with('title')
      ->andReturn('Test Page');

$model->title = 'Test Page';

I used a factory to create the model with the necessary attributes before applying the mock:

$model = MyModel::factory()->make([
    'title' => 'Test Page'
]);

$model = Mockery::mock($model)->shouldIgnoreMissing();

This approach allowed the null coalescing operator (??) to work correctly with the mocked object while maintaining the expected behavior for getAttribute.

Thanks to Flame's answer for pointing me in the right direction!

Upvotes: 0

Flame
Flame

Reputation: 7560

I think you're using the Model mock somewhat incorrectly by mocking getAttribute(). What often happens in tests is that you use a Laravel factory to create some mock model like $model = User::factory()->create(), which should set the properties correctly.

The behaviour you are seeing might be coming from the fact that internally in Eloquent models, all properties are actually part of the protected array $attributes array and due to some magic __get() they are retrieved.

So I believe that if you normally construct the mock object (so using YourModel::create() call through a factory or manually in a test) it should work as intended.

EDIT: now that I am writing this out you might be able to get away with mocking the protected $attributes property on the Eloquent model and set a property that way. But it is somewhat odd to do it that way.

Upvotes: 1

Related Questions