如何为Laravel缓存锁编写功能测试?
解决Laravel Cache Lock功能测试中429状态码的验证问题
你的测试代码无法触发429的核心原因是测试同步执行:第一个请求会完整执行完sleep(11)后释放锁,第二个请求发起时锁已不存在,自然不会触发超时异常。以下是三种可行的解决方案:
方案一:手动持有锁模拟并发场景
无需真实并发,直接在测试中提前获取并持有锁,模拟第一个进程占用锁的状态:
public function test_ensure_cache_lock_throw_429_status_code(): void { $order = PricingOrder::factory()->create([ 'transaction_status' => 'pending', 'user_id' => $this->customer->id, ]); // 手动获取锁并持有12秒(超过控制器中10秒的等待时间) $lock = Cache::lock('lock-payment', 12); $lock->get(); // 发起请求,此时锁被占用,会触发超时返回429 $payload = json_encode($this->set_payload(201, 'settlement')); $this->actingAs($this->customer) ->put(route('payment', $order->order_id), ['order' => $payload]) ->assertStatus(429); // 释放锁,避免影响其他测试用例 $lock->release(); }
方案二:并发请求模拟真实场景
通过Guzzle发送异步请求,模拟两个浏览器同时发起请求的真实场景:
use GuzzleHttp\Client; use GuzzleHttp\Promise; public function test_ensure_cache_lock_throw_429_status_code(): void { $order = PricingOrder::factory()->create([ 'transaction_status' => 'pending', 'user_id' => $this->customer->id, ]); // 初始化Guzzle客户端,带上会话和CSRF令牌 $client = new Client([ 'base_uri' => config('app.url'), 'headers' => [ 'Cookie' => $this->app['session']->getName() . '=' . $this->sessionId(), 'X-CSRF-TOKEN' => $this->app['session.store']->token(), ], ]); $payload = json_encode($this->set_payload(201, 'settlement')); $url = route('payment', $order->order_id); // 创建两个异步请求 $promise1 = $client->putAsync($url, ['form_params' => ['order' => $payload]]); $promise2 = $client->putAsync($url, ['form_params' => ['order' => $payload]]); // 等待两个请求完成 $responses = Promise\Utils::unwrap([$promise1, $promise2]); // 验证一个请求返回302,另一个返回429 $statusCodes = collect($responses)->map(fn($res) => $res->getStatusCode()); $this->assertContains(302, $statusCodes); $this->assertContains(429, $statusCodes); }
方案三:模拟异常抛出(单元测试思路)
如果只需验证异常处理逻辑,可直接模拟LockTimeoutException的抛出,无需依赖真实缓存锁:
use Mockery; use Illuminate\Contracts\Cache\Lock; use Illuminate\Cache\LockTimeoutException; public function test_ensure_cache_lock_throw_429_status_code(): void { $order = PricingOrder::factory()->create([ 'transaction_status' => 'pending', 'user_id' => $this->customer->id, ]); // 模拟Cache锁对象,调用block时抛出超时异常 $lockMock = Mockery::mock(Lock::class); $lockMock->shouldReceive('block') ->with(10, Mockery::any()) ->andThrow(new LockTimeoutException()); $this->app->make('cache')->shouldReceive('lock') ->with('lock-payment') ->andReturn($lockMock); $payload = json_encode($this->set_payload(201, 'settlement')); $this->actingAs($this->customer) ->put(route('payment', $order->order_id), ['order' => $payload]) ->assertStatus(429); }
各方案优缺点
- 方案一:简单高效,适合快速验证核心逻辑,无需额外依赖
- 方案二:完全贴近真实用户场景,但需处理会话和CSRF配置,稍复杂
- 方案三:属于单元测试范畴,专注于异常处理逻辑,不涉及真实缓存锁机制
内容的提问来源于stack exchange,提问作者nikkiSix
相关产品推荐
相关产品推荐

