在 Laravel 测试场景中,模拟多用户操作是验证权限逻辑、业务流程的常见需求,而切换用户后如果没有正确重置认证守卫,会导致后续测试用例的认证状态相互干扰,引发测试失败。

Laravel 认证守卫的基本工作原理
Laravel 的认证系统通过守卫(Guard)来定义用户认证的驱动和模型,默认配置中 web 守卫使用 session 驱动,api 守卫使用 token 驱动。在测试环境中,框架会模拟请求上下文,认证状态会绑定到当前测试的请求实例中。
当我们调用 actingAs 方法模拟用户登录时,实际上是给当前请求的守卫设置了认证用户,这个状态会保留到当前测试用例执行结束,如果没有主动重置,就可能影响后续的测试用例。
多用户切换的常见问题
很多开发者在测试中切换用户时,直接使用多次 actingAs 方法,例如下面的错误示例:
<?php
namespace TestsFeature;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use TestsTestCase;
class UserSwitchTest extends TestCase
{
use RefreshDatabase;
public function test_multi_user_switch_wrong()
{
$userA = User::factory()->create();
$userB = User::factory()->create();
// 第一次切换用户A
$this->actingAs($userA);
$responseA = $this->get('/api/user');
$responseA->assertJson(['id' => $userA->id]);
// 第二次切换用户B,没有重置守卫
$this->actingAs($userB);
$responseB = $this->get('/api/user');
// 这里可能出现状态混乱,导致断言失败
$responseB->assertJson(['id' => $userB->id]);
}
}
上面的代码在简单场景下可能暂时正常,但如果测试用例之间存在依赖,或者后续测试用例没有重新初始化认证状态,就会出现用户状态残留的问题。
正确重置认证守卫的实现方法
方法一:使用 logout 方法主动登出
在切换用户之前,先调用当前守卫的 logout 方法清除之前的认证状态,再切换新的用户:
<?php
namespace TestsFeature;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use TestsTestCase;
class UserSwitchTest extends TestCase
{
use RefreshDatabase;
public function test_multi_user_switch_with_logout()
{
$userA = User::factory()->create();
$userB = User::factory()->create();
// 切换用户A并验证
$this->actingAs($userA, 'web');
$responseA = $this->get('/api/user');
$responseA->assertJson(['id' => $userA->id]);
// 重置守卫:登出当前用户
auth('web')->logout();
// 切换用户B并验证
$this->actingAs($userB, 'web');
$responseB = $this->get('/api/user');
$responseB->assertJson(['id' => $userB->id]);
}
}
方法二:使用测试宏简化重置逻辑
如果需要频繁进行多用户切换,可以在测试用例中定义一个宏方法,统一处理守卫重置逻辑:
<?php
namespace TestsFeature;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use TestsTestCase;
class UserSwitchTest extends TestCase
{
use RefreshDatabase;
// 定义切换用户的宏方法
protected function switchUser(User $user, $guard = 'web')
{
// 先重置指定守卫的认证状态
auth($guard)->forgetUser();
// 再切换新用户
$this->actingAs($user, $guard);
}
public function test_multi_user_switch_with_macro()
{
$userA = User::factory()->create();
$userB = User::factory()->create();
$userC = User::factory()->create(['is_admin' => true]);
// 使用宏方法切换用户A
$this->switchUser($userA);
$this->get('/api/user')->assertJson(['id' => $userA->id]);
// 切换用户B
$this->switchUser($userB);
$this->get('/api/user')->assertJson(['id' => $userB->id]);
// 切换管理员用户到admin守卫
$this->switchUser($userC, 'admin');
$this->get('/api/admin/info')->assertJson(['id' => $userC->id]);
}
}
方法三:利用测试用例的 setUp 和 tearDown 方法
如果希望每个测试用例执行前后都自动重置认证状态,可以重写 setUp 和 tearDown 方法:
<?php
namespace TestsFeature;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use TestsTestCase;
class UserSwitchTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// 每个测试用例执行前重置所有守卫状态
auth()->guard('web')->forgetUser();
auth()->guard('api')->forgetUser();
auth()->guard('admin')->forgetUser();
}
protected function tearDown(): void
{
// 每个测试用例执行后也重置守卫状态
auth()->guard('web')->forgetUser();
auth()->guard('api')->forgetUser();
auth()->guard('admin')->forgetUser();
parent::tearDown();
}
public function test_multi_user_switch_with_lifecycle()
{
$userA = User::factory()->create();
$userB = User::factory()->create();
$this->actingAs($userA);
$this->get('/api/user')->assertJson(['id' => $userA->id]);
$this->actingAs($userB);
$this->get('/api/user')->assertJson(['id' => $userB->id]);
}
}
不同守卫场景下的切换注意事项
如果使用多个自定义守卫,切换用户时需要显式指定守卫名称,避免默认守卫状态混乱。例如 api 守卫使用 token 驱动时,切换用户需要同时处理 token 的传递:
<?php
namespace TestsFeature;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use TestsTestCase;
class ApiUserSwitchTest extends TestCase
{
use RefreshDatabase;
public function test_api_guard_user_switch()
{
$userA = User::factory()->create();
$userB = User::factory()->create();
// 切换api守卫的用户A
$tokenA = $userA->createToken('test')->plainTextToken;
$responseA = $this->withHeader('Authorization', 'Bearer ' . $tokenA)
->get('/api/user');
$responseA->assertJson(['id' => $userA->id]);
// 重置api守卫状态,切换用户B
$tokenB = $userB->createToken('test')->plainTextToken;
$responseB = $this->withHeader('Authorization', 'Bearer ' . $tokenB)
->get('/api/user');
$responseB->assertJson(['id' => $userB->id]);
}
}
总结
在 Laravel 测试中实现多用户切换的核心是保证每个用户切换操作前,对应的认证守卫状态已经被正确重置。可以根据项目需求选择主动登出、封装切换宏、利用测试用例生命周期方法等不同方案。同时要注意不同守卫的驱动差异,避免跨守卫的状态干扰,保证每个测试用例的独立性,让测试代码更加稳定可靠。