如何使用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
相关产品推荐
相关产品推荐

