为何模拟合法HasMany关联后过滤关联集合仍为空?
我正在编写单元测试,用来过滤模型中的无效关联,只保留模型实际存在的关联。但测试失败了——过滤后的关联集合是空的,尽管我已经模拟了其中一个关联validRelation,让它返回合法的HasMany关联。
额外背景
- 使用Laravel和Mockery进行单元测试。
filterRestrictedRelations方法为protected,通过反射调用。- 尝试将该方法设为public并移除反射逻辑后,集合仍为空,问题似乎出在Mockery对
shouldReceive('validRelation')的处理上。
核心代码
filterRestrictedRelations方法:
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; }); }
测试代码:
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()); }
Mockery的shouldReceive()并不会真正在模拟对象上创建对应的方法,它只是设置了方法调用的预期规则。而method_exists()是PHP原生函数,只会检查对象的类结构中是否存在该方法,完全识别不了Mockery动态设置的方法预期,所以哪怕你用shouldReceive('validRelation')做了模拟,method_exists($this->model, 'validRelation')依然会返回false,最终导致所有关联都被过滤清空。
另外测试代码里还有个小问题:setUp中你mock了modelRelation,但测试方法里又重新实例化了new ModelRelation($this->model),不过这不是导致集合为空的核心原因。
针对这个问题,有两种可行的解决思路:
思路1:让模拟对象真正拥有目标方法
如果是用makePartial()创建的模拟对象,可以直接给它添加方法定义,而非仅设置调用预期:
// 替换原来的shouldReceive代码 $this->model = \Mockery::mock(AnAbstractClass::class.'[validRelation]')->makePartial(); $this->model->shouldReceive('validRelation')->andReturn(\Mockery::mock(HasMany::class));
这种方式下,method_exists()就能检测到validRelation方法存在,过滤逻辑会正常保留该关联。
思路2:修改过滤逻辑适配测试场景
如果不想调整模拟方式,可以把method_exists()替换成Laravel Eloquent模型自带的hasRelation()方法:
// 修改filterRestrictedRelations中的判断逻辑 $exists = $this->model->hasRelation($relation);
同时在测试中手动给模型添加关联记录:
// 在测试中添加关联到模型的relations属性 $this->model->relations['validRelation'] = \Mockery::mock(HasMany::class);
hasRelation()会检查模型的$relations属性,这样就能正确识别关联是否存在,过滤逻辑也能正常工作。
测试代码小修正
另外注意测试里的注释写错了,应该是“只保留validRelation,其他关联被过滤掉”,和断言逻辑保持一致。
内容的提问来源于stack exchange,提问作者csandreas1

