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

如何为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 06:35:27