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?
Note that I am using PHP 8.2, phpUnit 10.5 with Laravel 11.33.2
2
Answers
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:
I used a factory to create the model with the necessary attributes before applying the mock:
This approach allowed the null coalescing operator (
??
) to work correctly with the mocked object while maintaining the expected behavior forgetAttribute
.Thanks to Flame's answer for pointing me in the right direction!
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.