Symfony中PHPUnit mock仓库方法返回bool值报错如何解决
问题场景
在Symfony项目中使用"phpunit/phpunit": "^9.5"编写单元测试时,需要对WorkspaceMemberRepository类的hasRole方法模拟返回值,仓库类原代码如下:
class WorkspaceMemberRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, WorkspaceMember::class); } public function hasRole( int $memberId, string $action, string $subject ): bool { return 0 !== (int)$this ->createQueryBuilder('wm') ->select('count(1)') ->join('App\Shared\Entity\Role', 'r', Join::WITH, 'wm.workspace = r.workspace') ->join('App\Shared\Entity\RoleAbility', 'ra', Join::WITH, 'r.id = ra.role') ->where('wm.member = :memberId') ->andWhere('ra.action = :action') ->andWhere('ra.subject = :subject') ->setParameters([ 'memberId' => $memberId, 'action' => $action, 'subject' => $subject ]) ->getQuery() ->getSingleScalarResult(); } }
初始编写的Mock代码如下:
$workspaceMemberRepository = $this->createMock(WorkspaceMemberRepository::class); $workspaceMemberRepository->expects($this->any()) ->method('hasRole') ->willReturn(true); $this->assertTrue($workspaceMemberRepository->hasRole(1, 'edit', 'customers'));
运行测试时报错:
Return value must be of type bool, none returned(返回值必须为bool类型,但实际未返回任何值)
问题原因
报错核心是WorkspaceMemberRepository继承的Doctrine ServiceEntityRepository父类(EntityRepository)内置了__call魔术方法,直接调用createMock()生成模拟对象时,PHPUnit默认不会显式声明要模拟的方法列表,会出现方法匹配失败的问题:配置的willReturn(true)规则没有生效,PHPUnit转而尝试执行原类的hasRole方法。但生成模拟对象时默认禁用了原构造函数,类内部依赖的Doctrine实体管理器属性为null,原方法执行到数据库查询逻辑时直接中断,没有返回任何值,最终触发返回类型不匹配的错误。
解决方法
方法1:显式指定要模拟的方法列表(无侵入,快速修复)
不需要改动原有业务代码,把createMock()替换为getMockBuilder(),通过onlyMethods()显式指定要模拟的方法名即可:
$workspaceMemberRepository = $this->getMockBuilder(WorkspaceMemberRepository::class) ->disableOriginalConstructor() ->onlyMethods(['hasRole']) // 仅模拟hasRole方法 ->getMock(); $workspaceMemberRepository->expects($this->any()) ->method('hasRole') ->willReturn(true); // 验证模拟生效 $this->assertTrue($workspaceMemberRepository->hasRole(1, 'edit', 'customers'));
注意:PHPUnit 9+版本使用onlyMethods()指定模拟方法,如果你使用的是PHPUnit 8及更早版本,替换为setMethods(['hasRole'])即可。
方法2:面向接口抽象(符合Symfony最佳实践)
如果追求代码长期可维护性,建议给Repository类抽离独立接口,业务代码依赖接口而非具体实现类,测试时直接模拟接口,完全避开Doctrine父类的兼容问题:
- 定义Repository接口
interface WorkspaceMemberRepositoryInterface { public function hasRole(int $memberId, string $action, string $subject): bool; }
- 原有仓库类实现该接口
class WorkspaceMemberRepository extends ServiceEntityRepository implements WorkspaceMemberRepositoryInterface { // 原有代码不需要做任何改动 }
- 在Symfony服务配置中完成接口到实现类的绑定后,测试代码可以直接模拟接口:
$workspaceMemberRepository = $this->createMock(WorkspaceMemberRepositoryInterface::class); $workspaceMemberRepository->expects($this->any()) ->method('hasRole') ->willReturn(true); $this->assertTrue($workspaceMemberRepository->hasRole(1, 'edit', 'customers'));
这种写法不会受Doctrine父类内部实现(比如魔术方法、继承逻辑)的影响,测试代码稳定性更高。
内容的提问来源于stack exchange,提问作者yaraw69

