Our Location

304 North Cardinal St.
Dorchester Center, MA 02124

使用 Pest 進行 Laravel 多執行緒測試:挑戰 API 限制

在 Web 應用程式開發中,確保 API 的穩定性和效能至關重要。有時,API 會有並行處理的限制,例如,為了防止資源競爭或資料不一致,同一個 API 在同一時間只能被一個請求呼叫。這種情況下,傳統的循序測試方法可能不足以驗證 API 的行為。這時候,我們需要使用多執行緒測試。

在 Web 應用程式開發中,確保 API 的穩定性和效能至關重要。有時,API 會有並行處理的限制,例如,為了防止資源競爭或資料不一致,同一個 API 在同一時間只能被一個請求呼叫。這種情況下,傳統的循序測試方法可能不足以驗證 API 的行為。這時候,我們需要使用多執行緒測試。

這篇文章將會介紹兩種在 Laravel 中使用 Pest 進行多執行緒測試的方法:

  1. 使用 pcntl 擴充套件
  2. 使用 Laravel 的並行測試功能

我們將使用「購物車下單與庫存數」作為範例。假設我們有一個 API 端點 /api/checkout,它接受 product_idquantity 作為參數,並嘗試下單。為了防止超賣,我們需要確保即使多個請求同時到達,庫存也能正確處理。

方法一:使用 pcntl 擴充套件

pcntl (Process Control) 是 PHP 的一個擴充套件,它提供了一系列用於處理 Unix 行程的函式。我們可以使用 pcntl 來建立多個子行程,每個子行程執行一個測試任務,從而模擬並行下單的場景。

範例程式碼:

des\DB;

// 假設我們的 API 端點是 /api/checkout
$apiEndpoint = '/api/checkout';

test('API 應限制並行購買請求 (使用 pcntl)', function () {
    $processes = [];
    $numRequests = 5; // 模擬 5 個並行請求
    $productId = 1;
    $initialStock = 10; // 假設產品初始庫存為 10

    // 建立測試資料
    DB::table('products')->insert([
        'id' => $productId,
        'stock' => $initialStock,
    ]);

    // 建立多個子行程
    for ($i = 0; $i < $numRequests; $i++) {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die("無法 fork 行程");
        } elseif ($pid) {
            // 父行程:儲存子行程 ID
            $processes[] = $pid;
        } else {
            // 子行程:發送 API 請求
            // 使用 Laravel 的 HTTP 測試功能
            $response = test()->postJson($apiEndpoint, ['product_id' => $productId, 'quantity' => 1]);

            // 這裡我們不直接斷言,而是將結果回傳
            // 使用檔案來回傳結果
            if ($response->getStatusCode() == 200) {
                file_put_contents("/tmp/success_{$i}", "success");
            } else {
                file_put_contents("/tmp/fail_{$i}", "fail");
            }
            exit(0); // 子行程結束
        }
    }

    // 父行程:等待所有子行程結束
    foreach ($processes as $pid) {
        pcntl_waitpid($pid, $status);
    }

    // 驗證結果
    $successCount = 0;
    $failCount = 0;
    for ($i = 0; $i < $numRequests; $i++) {
        if (file_exists("/tmp/success_{$i}")) {
            $successCount++;
            unlink("/tmp/success_{$i}");
        }
        if (file_exists("/tmp/fail_{$i}")) {
            $failCount++;
            unlink("/tmp/fail_{$i}");
        }
    }

    // 判斷只有一個成功,並且庫存減少 1
    expect($successCount)->toBe(1);
    expect($failCount)->toBe($numRequests - 1);

    $finalStock = DB::table('products')->where('id', $productId)->value('stock');
    expect($finalStock)->toBe($initialStock - 1);
});

程式碼說明:

  1. pcntl_fork(): 建立一個新的行程。父行程和子行程都會繼續執行後面的程式碼。
  2. 子行程: 子行程中使用 Laravel 的 HTTP 測試功能向 /api/checkout 端點發送 POST 請求,模擬下單。
  3. 父行程: 父行程等待所有子行程執行完成。
  4. 驗證結果:
    • 父行程檢查子行程的執行結果,判斷只有一個請求成功。
    • 父行程檢查資料庫,驗證庫存是否正確減少。

方法二:使用 Laravel 的並行測試功能

Laravel 提供了內建的並行測試功能,您可以使用 php artisan test --parallel 命令來執行測試。--processes 選項允許您指定要使用的行程數。

範例程式碼:

首先,我們需要建立一個普通的 Pest 測試案例:

<?php

use function Pest\test;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\DB;

// 假設我們的 API 端點是 /api/checkout
$apiEndpoint = '/api/checkout';

test('API 應限制並行購買請求 (使用 Laravel 並行測試)', function () {
    $productId = 1;
    $initialStock = 10;

     // 建立測試資料
    DB::table('products')->insert([
        'id' => $productId,
        'stock' => $initialStock,
    ]);

    // 模擬發送請求
    $responses = [];
    for ($i = 0; $i < 5; $i++) {
        $responses[] = Http::post($apiEndpoint, ['product_id' => $productId, 'quantity' => 1]);
    }

    $successCount = 0;
    $failCount = 0;
    foreach ($responses as $response) {
        if ($response->getStatusCode() == 200) {
            $successCount++;
        } else {
            $failCount++;
        }
    }

     // 判斷只能有一個成功,並且庫存減少 1
    expect($successCount)->toBe(1);
    expect($failCount)->toBe(4);

    $finalStock = DB::table('products')->where('id', $productId)->value('stock');
    expect($finalStock)->toBe($initialStock - 1);
});

然後,在終端機中執行以下命令:

php artisan test --parallel --processes=4

這將會使用 4 個行程來並行執行您的測試。

兩種方法的比較

特性pcntlLaravel 並行測試
易用性
較複雜,需要自行管理行程的建立、執行和同步。更簡單,使用 php artisan test --parallel 命令即可輕鬆實現。
資料庫處理
需要自行處理資料庫隔離,例如使用交易或為每個行程建立單獨的連線。自動為每個行程建立一個測試資料庫,確保測試之間的資料隔離。
效能
可以更靈活地控制行程的行為,可能在某些特定場景下有更好的效能。針對一般測試場景進行了優化,可以更有效地利用系統資源。
適用場景適用於需要精確控制並行行為的特定測試場景,
例如模擬多個使用者同時與 API 互動並驗證其並行處理邏輯。
適用於加速一般的測試執行,特別是對於測試套件執行時間較長的專案。
伺服器依賴需要伺服器支援 pcntl 擴充套件。對伺服器環境的要求較低

總結

  • php artisan test --parallel --processes=4 是一種更簡單、更高效的方式來並行執行 Laravel 測試,特別是對於加速測試套件的執行。
  • 對於需要更精細地控制並行行為的特定測試場景(例如,模擬多個使用者同時與 API 互動並驗證其並行處理邏輯),pcntl(如範例所示)提供了更大的靈活性。