Symfony自定义Voter类单元测试失败如何正确排查
核心错误点
你的测试跑不通是几个问题叠加导致的:
getUser()mock返回值类型错误:你在setUp()里给Token mock的getUser()方法返回的是普通数组,但业务代码里明确判断$user不是UserInterface实例时直接返回false,数组永远通不过这个类型校验,所有登录态校验都会失败。- DataProvider执行时机错误:PHPUnit的
provideCases()运行在setUp()之前,你在数据源里引用$this->token时,这个类属性还没被初始化,值就是null,和你报错信息里显示的第三个入参为null完全对应。而且你的testVote方法根本没使用参数里传入的$token,一直固定调用$this->token,参数定义完全无效。 - 测试用例逻辑矛盾:你写的「用户可读」「用户不可读」两个场景,传入的所有参数完全一致,只有预期结果不同,没有区分有权限/无权限的前置条件,不可能测出两种不同结果。
- Subject/属性不匹配导致Voter弃权:Voter的
vote()方法返回-1对应常量VoterInterface::ACCESS_ABSTAIN,意思是当前Voter不处理这个请求。出现这个返回值要么是你传入的attribute不在Voter支持的列表里,要么是subject类型不符合supports()方法的校验规则(比如你传了字符串'customers',但supports()要求传入Customer实体实例),此时根本不会执行到voteOnAttribute逻辑。 - 业务代码本身有遗漏:你贴的
voteOnAttribute代码里,match分支调用canRead()、canCreate()时传入的$member变量没有定义,运行时会直接报变量未定义错误。
修正后的测试代码示例
不要在setUp里全局构造固定的Token mock,每个测试场景根据需要单独构造依赖,避免数据源提前执行导致的空值问题:
<?php use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\User\UserInterface; class RoleVoterTest extends TestCase { /** * @dataProvider provideCases */ public function testVote( array $attributes, mixed $subject, ?UserInterface $mockUser, int $expectedResult ): void { // 每个用例单独构造Token,避免全局状态污染 $token = $this->createMock(TokenInterface::class); $token->method('getUser') ->willReturn($mockUser); $voter = new RoleVoter(); $this->assertSame($expectedResult, $voter->vote($token, $subject, $attributes)); } public function provideCases(): \Generator { // 场景1:未登录用户访问,拒绝 yield 'guest user denied for read' => [ ['read'], 'customers', // 如果supports要求传实体实例,这里换成对应mock即可 null, VoterInterface::ACCESS_DENIED, ]; // 场景2:登录但无权限用户访问,拒绝 $noPermUser = $this->createMock(UserInterface::class); // 如果canRead逻辑需要调用User的方法(比如获取角色、所属组),在这里给mock配置对应返回值 yield 'normal user denied for read without permission' => [ ['read'], 'customers', $noPermUser, VoterInterface::ACCESS_DENIED, ]; // 场景3:有权限用户访问,允许 $permUser = $this->createMock(UserInterface::class); // 同上,配置canRead需要的mock返回值,比如给角色字段设为管理员等 yield 'permission user granted for read' => [ ['read'], 'customers', $permUser, VoterInterface::ACCESS_GRANTED, ]; // 场景4:传入Voter不支持的属性,弃权返回-1 yield 'unsupported attribute trigger abstain' => [ ['invalid_attr'], 'customers', $permUser, VoterInterface::ACCESS_ABSTAIN, ]; } }
额外注意
先补全你业务代码里voteOnAttribute方法中未定义的$member变量逻辑,确认这个变量是从$user对象获取还是通过其他依赖注入得到,再在测试里对应配置mock的返回值即可。如果supports()方法对subject类型有要求,把测试里传的字符串'customers'换成对应类型的mock/实例,不然会一直返回-1的弃权结果。
内容的提问来源于stack exchange,提问作者yaraw69
相关产品推荐
相关产品推荐

