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

如何使用Pest测试Laravel应用中的竞争条件(支持不同请求体的并发请求)

如何使用Pest测试Laravel应用中的竞争条件(支持不同请求体的并发请求)

嘿,我完全懂你的困扰!想用Pest测试Laravel应用的竞争条件,但自带的方法要么是挨个发请求(完全没并发效果),要么是stress()重复发同一个请求体,根本满足不了你测不同请求并发的需求对吧?别慌,我给你分享两个实用的方案,能搞定这个问题~

方案一:用Guzzle并发请求池发送不同请求体

默认的Pest/Laravel测试客户端是在同一个进程里模拟请求的,没法实现真实的并发。所以我们可以用Guzzle的并发请求池,直接向运行中的Laravel服务器发送真实的HTTP请求,每个请求带不同的payload。

具体实现代码

use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

test('测试不同请求体的并发竞争条件', function () {
    // 先确保你的Laravel服务器在运行,比如用`php artisan serve`启动在8000端口
    $serverUrl = 'http://localhost:8000';

    // 准备你需要测试的不同请求体
    $payload1 = ['key' => 'value1', 'data' => 'test1'];
    $payload2 = ['key' => 'value2', 'data' => 'test2'];

    $client = new Client();

    // 构建多个不同的POST请求
    $requests = [
        new Request(
            'POST',
            $serverUrl . action('YourController@yourAction'),
            ['Content-Type' => 'application/json'],
            json_encode($payload1)
        ),
        new Request(
            'POST',
            $serverUrl . action('YourController@yourAction'),
            ['Content-Type' => 'application/json'],
            json_encode($payload2)
        ),
        // 还可以继续添加更多不同的请求
    ];

    // 创建并发请求池,设置并发数等于请求数量
    $pool = new Pool($client, $requests, [
        'concurrency' => count($requests),
        'fulfilled' => function ($response, $index) {
            // 验证每个请求都成功返回
            expect($response->getStatusCode())->toBe(200);
        },
        'rejected' => function ($reason, $index) {
            // 如果请求失败,抛出异常终止测试
            throw new Exception("第 {$index} 个请求失败: {$reason->getMessage()}");
        },
    ]);

    // 启动所有请求并等待全部完成
    $promise = $pool->promise();
    $promise->wait();

    // 这里添加核心断言,检查竞争条件是否触发了预期问题
    // 比如验证数据库中的数据是否符合预期(有没有重复、数据不一致等)
    $recordCount = YourModel::count();
    expect($recordCount)->toBe(2); // 示例断言,根据你的业务调整
});

方案二:用PHP多进程(pcntl扩展)实现并发

如果你需要更底层的并发控制,可以用PHP的pcntl扩展创建子进程,每个子进程单独发送一个带不同payload的请求。

具体实现代码

use GuzzleHttp\Client;

test('用多进程测试不同请求体的竞争条件', function () {
    $serverUrl = 'http://localhost:8000';
    // 准备不同的请求体数组
    $payloads = [
        ['key' => 'value1', 'data' => 'test1'],
        ['key' => 'value2', 'data' => 'test2'],
    ];
    $childPids = [];

    foreach ($payloads as $payload) {
        $pid = pcntl_fork();
        if ($pid === -1) {
            throw new Exception('无法创建子进程,请确保已启用pcntl扩展');
        } elseif ($pid === 0) {
            // 子进程逻辑:发送请求
            $client = new Client();
            $response = $client->post(
                $serverUrl . action('YourController@yourAction'),
                ['json' => $payload]
            );
            // 子进程退出,返回状态码
            exit($response->getStatusCode());
        } else {
            // 父进程记录子进程ID
            $childPids[] = $pid;
        }
    }

    // 等待所有子进程完成,并验证每个请求的状态
    foreach ($childPids as $pid) {
        pcntl_waitpid($pid, $status);
        expect(pcntl_wexitstatus($status))->toBe(200);
    }

    // 同样添加竞争条件的核心断言
    // ...
});

额外小贴士:自动启动临时测试服务器

如果你不想每次测试前手动启动Laravel服务器,可以用Laravel自带的Process类(基于Symfony Process)在测试中自动启动和关闭临时服务器:

use Symfony\Component\Process\Process;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

test('自动启动服务器测试竞争条件', function () {
    // 启动临时服务器,用8001端口避免和本地服务冲突
    $process = new Process(['php', 'artisan', 'serve', '--port', '8001']);
    $process->start();

    // 给服务器预留2秒启动时间
    sleep(2);

    try {
        $serverUrl = 'http://localhost:8001';
        // 这里放前面的并发请求代码,把地址改成上面的临时服务器地址
        // ...
    } finally {
        // 不管测试成功还是失败,都关闭临时服务器
        $process->stop();
    }
});

注意事项

  1. 一定要用测试数据库,避免竞争条件测试影响生产或开发数据;
  2. 竞争条件测试可能不稳定,因为并发时机不可控,可能需要多次运行才能复现问题;
  3. 如果使用pcntl扩展,要确保你的PHP环境已经启用了这个扩展(通常在CLI环境下可用);
  4. 临时服务器的启动时间可能需要调整,如果服务器启动慢,可以适当延长sleep的时间。

备注:内容来源于stack exchange,提问作者mankowitz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 17:38:07