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

如何使用PHPUnit测试体内包含不可模拟类的方法?

如何测试依赖静态数据库查询的userPermissionGranted方法

看起来你遇到的问题是静态调用UserPermission的查询逻辑导致难以模拟,这在测试中是很常见的痛点——静态硬编码依赖会直接把测试和数据库强绑定,让单元测试变得棘手。下面我给你几个可行的解决方案,从优雅重构到快速临时测试都覆盖到了:

方案1:重构代码,通过依赖注入解耦(推荐长期方案)

最根本的解决办法是把UserPermission的查询逻辑从静态调用改成依赖注入,这样测试时就能轻松替换成模拟对象,彻底摆脱数据库依赖。

步骤1:调整原方法,注入UserPermission模型实例

// 假设你的业务类是PermissionChecker
class PermissionChecker {
    private $userPermissionModel;

    // 通过构造函数注入UserPermission模型
    public function __construct(UserPermission $userPermissionModel) {
        $this->userPermissionModel = $userPermissionModel;
    }

    /**
     * 判断指定用户是否被授予指定权限。
     *
     * @param User $user
     * @param AbstractPermission $permission
     * @return bool
     */
    public function userPermissionGranted(User $user, AbstractPermission $permission) : bool {
        // 使用注入的实例调用scope方法,替代静态调用
        $user_permission = $this->userPermissionModel
            ->scopeUser($user)
            ->scopePermission($permission)
            ->first();

        return !is_null($user_permission);
    }
}

步骤2:PHPUnit测试中模拟UserPermission行为

现在你可以用PHPUnit原生的模拟功能,轻松控制UserPermission的查询返回结果:

use PHPUnit\Framework\TestCase;
use App\Models\User;
use App\Models\UserPermission;
use App\Permissions\AbstractPermission;
use App\Services\PermissionChecker;

class PermissionCheckerTest extends TestCase {
    public function testUserPermissionGrantedReturnsTrueWhenPermissionExists() {
        // 1. 创建测试用的User和Permission模拟实例
        $user = $this->createMock(User::class);
        $permission = $this->createMock(AbstractPermission::class);

        // 2. 模拟UserPermission的链式查询行为
        $userPermissionMock = $this->createMock(UserPermission::class);
        // 模拟scopeUser调用后返回自身(满足链式调用)
        $userPermissionMock->method('scopeUser')
            ->with($user)
            ->willReturnSelf();
        // 模拟scopePermission调用后返回自身
        $userPermissionMock->method('scopePermission')
            ->with($permission)
            ->willReturnSelf();
        // 模拟first()返回非null值,表示权限存在
        $userPermissionMock->method('first')
            ->willReturn(new UserPermission());

        // 3. 初始化被测试类,注入模拟对象
        $checker = new PermissionChecker($userPermissionMock);

        // 4. 断言结果符合预期
        $this->assertTrue($checker->userPermissionGranted($user, $permission));
    }

    public function testUserPermissionGrantedReturnsFalseWhenPermissionDoesNotExist() {
        $user = $this->createMock(User::class);
        $permission = $this->createMock(AbstractPermission::class);

        $userPermissionMock = $this->createMock(UserPermission::class);
        $userPermissionMock->method('scopeUser')->with($user)->willReturnSelf();
        $userPermissionMock->method('scopePermission')->with($permission)->willReturnSelf();
        // 模拟first()返回null,表示权限不存在
        $userPermissionMock->method('first')->willReturn(null);

        $checker = new PermissionChecker($userPermissionMock);

        $this->assertFalse($checker->userPermissionGranted($user, $permission));
    }
}

方案2:不重构代码,直接模拟静态查询(临时快速方案)

如果暂时不想改动业务代码,你可以用Mockery的mockStatic功能来模拟静态方法调用(注意:这种方式会让测试和静态代码强耦合,长期来看还是推荐重构):

use PHPUnit\Framework\TestCase;
use Mockery;
use App\Models\User;
use App\Models\UserPermission;
use App\Permissions\AbstractPermission;
use App\Services\PermissionChecker;

class PermissionCheckerTest extends TestCase {
    protected function tearDown(): void {
        Mockery::close(); // 清理Mockery的静态模拟,避免影响其他测试
    }

    public function testUserPermissionGrantedWithStaticMock() {
        $user = $this->createMock(User::class);
        $permission = $this->createMock(AbstractPermission::class);

        // 模拟查询链的中间对象
        $queryMock = Mockery::mock();
        $queryMock->shouldReceive('scopeUser')->with($user)->andReturnSelf();
        $queryMock->shouldReceive('scopePermission')->with($permission)->andReturnSelf();
        $queryMock->shouldReceive('first')->andReturn(new UserPermission());

        // 模拟UserPermission的静态调用返回上面的查询对象
        Mockery::mockStatic(UserPermission::class, function ($mock) use ($queryMock) {
            $mock->shouldReceive('scopeUser')->andReturn($queryMock);
        });

        $checker = new PermissionChecker();
        $this->assertTrue($checker->userPermissionGranted($user, $permission));
    }
}

方案3:如果是Laravel项目,用框架自带的查询模拟

如果你的项目基于Laravel,框架提供了更便捷的Eloquent查询模拟方式,不需要额外配置Mockery:

use Tests\TestCase;
use App\Models\User;
use App\Models\UserPermission;
use App\Permissions\AbstractPermission;
use App\Services\PermissionChecker;

class PermissionCheckerTest extends TestCase {
    public function testUserPermissionGranted() {
        $user = User::factory()->create();
        $permission = $this->createMock(AbstractPermission::class);

        // 模拟链式查询的返回结果
        UserPermission::shouldReceive('scopeUser->scopePermission->first')
            ->with($user, $permission)
            ->andReturn(new UserPermission());

        $checker = app(PermissionChecker::class);
        $this->assertTrue($checker->userPermissionGranted($user, $permission));
    }
}

总结

  • 优先选方案1:依赖注入让代码更符合SOLID原则,可测试性和可维护性都会提升,是长期最优解。
  • 方案2适合临时快速验证,但静态模拟容易带来测试耦合,不建议长期使用。
  • 方案3是Laravel项目专属的简化方式,利用框架特性可以少写很多模拟代码。

内容的提问来源于stack exchange,提问作者minitauros

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 07:43:31