如何让PHP中Mock对象与空合并运算符(??)正常协同工作?
问题:Mockery Mock对象与空合并运算符(??)兼容问题
在使用Mockery编写PHPUnit测试时,遇到空合并运算符(??)与Mock属性协同工作的问题:直接调用$model->title能返回Mock的值,但使用$model->title ?? 'default'时总是触发默认值回退,仿佛属性不存在或为null。
测试代码
public function testExample() { $model = Mockery::mock(Model::class)->shouldIgnoreMissing(); // Mocking a property $model->shouldReceive('getAttribute') ->with('title') ->andReturn('Test Page'); $model->title = 'Test Page'; // 尝试过的另一种方式 $result = $this->doSomething($model); $this->assertEquals('/test-page', $result); } public function doSomething($model): string { print_r([$model->title, $model->title ?? 'default']); // ... 其他逻辑 }
输出结果
Array ( [0] => Test Page [1] => default )
环境栈:PHP 8.2,PHPUnit 10.5搭配Laravel 11.33.2
注:无需测试模型或Laravel框架本身,仅需让模型返回特定值,不想使用工厂及关联。
解决方案
原因分析
Laravel模型的属性访问依赖__get魔术方法,但空合并运算符(??)会优先调用__isset方法判断属性是否实际存在于对象中,而非仅检查__get的返回值。Mockery默认不会为Mock的属性注册__isset的返回逻辑,因此??会判定属性不存在,触发默认值。
方法一:Mock __isset 方法
直接为目标属性mock__isset方法,让空合并运算符认为属性存在:
public function testExample() { $model = Mockery::mock(Model::class)->shouldIgnoreMissing(); // Mock getAttribute 返回指定值 $model->shouldReceive('getAttribute') ->with('title') ->andReturn('Test Page'); // Mock __isset 让title被判定为存在 $model->shouldReceive('__isset') ->with('title') ->andReturn(true); $result = $this->doSomething($model); $this->assertEquals('/test-page', $result); }
方法二:使用Laravel自带的MockBuilder
Laravel的MockBuilder已封装模型魔术方法的兼容逻辑,无需手动处理__isset:
public function testExample() { $model = $this->mock(Model::class, function ($mock) { $mock->shouldReceive('getAttribute') ->with('title') ->andReturn('Test Page'); // 或直接设置属性,Laravel Mock会自动处理 $mock->title = 'Test Page'; }); $result = $this->doSomething($model); $this->assertEquals('/test-page', $result); }
方法三:用匿名类替代Mock(简单场景首选)
如果仅需让模型返回固定值,直接创建匿名类模拟模型,避开魔术方法问题:
public function testExample() { $model = new class extends Model { public $title = 'Test Page'; }; $result = $this->doSomething($model); $this->assertEquals('/test-page', $result); }
内容的提问来源于stack exchange,提问作者Waqleh
相关产品推荐
相关产品推荐

