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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:54:29