You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 21:39:29