Symfony PHPUnit测试Voter时TokenInterface::getUser未调用报错
问题根因
两个核心问题导致报错:
- PHPUnit生命周期不匹配:
dataProvider的执行时机早于setUp()方法,你在provideCases()中直接引用$this->token时,setUp还未运行,此时$this->token还是类定义时初始的null值,传入测试方法的根本不是你后续mock生成的Token对象,Voter自然不会调用到mock对象的getUser方法。 - Mock约束写死不合理:你在setUp中给Token mock写了
getUser必须被调用1次的强约束,但只要Voter的supports()方法返回false、或者传入的token非法,就不会进入voteOnAttribute流程,自然不会触发getUser调用,直接触发调用次数不匹配的报错。 - 额外隐患:当前
supports()方法只校验了属性名,没有校验subject类型,会导致所有带edit属性的判断都会进入当前Voter,容易引发非预期的类型错误。
修复方案
1. 调整测试结构,规避生命周期问题
不要在dataProvider中直接引用类属性,把Token mock、Repository mock的逻辑放到测试方法内部执行,dataProvider只传递用例的场景参数即可。
2. 移除不合理的Mock调用次数约束
不要在setUp阶段写死expects($this->once())这类强次数约束,改用method()默认的任意次调用匹配,避免非预期流程下的报错。
3. 补全Voter的supports校验逻辑
增加subject类型判断,避免无关subject进入权限判断逻辑。
修改后的测试代码示例
private $entityManager; private $voter; private $myCustomRepository; public function setUp(): void { $kernel = self::bootKernel(); $this->entityManager = $kernel->getContainer() ->get('doctrine') ->getManager(); // 初始化Voter依赖,不要在setUp里写mock调用预期 $this->myCustomRepository = $this->createMock(MyCustomRepository::class); $this->voter = new CustomVoter($this->myCustomRepository); } /** * @dataProvider provideCases */ public function testVote(array $attributes, mixed $subject, ?string $userEmail, int $expectedVote, bool $hasAccess = false) { $token = null; // 根据用例参数构造对应场景的Token if ($userEmail !== null) { $user = $this->entityManager ->getRepository(Member::class) ->findOneBy(['email' => $userEmail]); $token = $this->createMock(TokenInterface::class); $token->method('getUser') ->willReturn($user); } // 配置Repository返回预期 $this->myCustomRepository->method('hasAccess') ->willReturn($hasAccess); $this->assertEquals($expectedVote, $this->voter->vote($token, $subject, $attributes)); } public function provideCases(): \Generator { yield 'user can edit' => [ ['edit'], 'my_subject', 'user@test.com', VoterInterface::ACCESS_GRANTED, true ]; yield 'guest cannot edit' => [ ['edit'], 'my_subject', null, VoterInterface::ACCESS_DENIED ]; yield 'unsupported attribute will abstain' => [ ['view'], 'my_subject', 'user@test.com', VoterInterface::ACCESS_ABSTAIN ]; }
优化后的Voter supports方法
protected function supports(string $attribute, $subject): bool { if (!in_array($attribute, ['edit'])) { return false; } // 根据你的业务场景替换subject校验规则,比如判断是否是特定实体实例 if (!is_string($subject)) { return false; } return true; }
原报错触发逻辑:dataProvider提前执行拿到null值的token传入测试,Voter未进入
voteOnAttribute流程,和你提前写死的getUser必须调用1次的mock约束冲突,最终抛出调用次数不匹配的错误。
内容的提问来源于stack exchange,提问作者yaraw69
相关产品推荐
相关产品推荐

