API 自動化測試與效能優化

更新日期: 2025 年 2 月 13 日

在開發「商品描述優化 API」時,確保 API 穩定、高效運行至關重要。

透過 自動化測試,我們可以驗證 API 的輸出是否正確,並確保其能夠應對各種異常情境。

效能優化 則能提升 API 的處理速度,減少資源消耗,讓 API 能夠應對更大規模的請求量。


API 自動化測試

為什麼要進行 API 測試?

API 測試的主要目標是:

  • 確保 API 輸出符合預期(如 JSON 格式正確、關鍵欄位存在)
  • 驗證 API 是否能正確處理異常輸入(如空值、錯誤格式)
  • 測試 API 效能,確保在高併發情境下仍能穩定運行

我們使用 pytestFastAPI TestClient 來編寫測試,並透過 benchmark 進行效能分析。

基本 API 測試

📌 測試目標

我們的 API 旨在 優化商品描述,將輸入的商品資訊轉換為 標題、摘要和重點條列

為確保 API 功能正常,基本測試需要驗證:

  • API 是否能正確處理請求並返回 200 狀態碼
  • API 回應是否符合 JSON 格式
  • 回應中是否包含 summarized_description 這個關鍵欄位
  • 回應是否包含 title(標題)、summary(摘要)、bullet_points(商品重點)等欄位

📌 測試程式碼

from fastapi.testclient import TestClient
from optimize_description import app

# 初始化 FastAPI 測試客戶端
client = TestClient(app)
URL = "/summarized-description"

def test_summarize_description():
    """ 測試 API 是否能正確處理商品描述優化 """
    # 測試請求的 JSON 內容
    payload = {"description": "這款智能手錶具備心率監測與 GPS 追蹤,適合運動健身。"}
    
    # 發送 POST 請求到 API
    response = client.post(URL, json=payload)

    # 確保 API 回應狀態碼為 200(成功)
    assert response.status_code == 200
    
    # 解析回應內容為 JSON
    data = response.json()

    # 確保 API 回應中包含 `summarized_description` 欄位
    assert "summarized_description" in data  

    # 確保 `summarized_description` 內部包含必要欄位
    assert "title" in data["summarized_description"]  # 標題
    assert "summary" in data["summarized_description"]  # 簡要說明
    assert "bullet_points" in data["summarized_description"]  # 商品重點條列

    # 確保 `bullet_points` 為一個列表
    assert isinstance(data["summarized_description"]["bullet_points"], list)

📌 測試解析

這段測試程式碼的流程如下:

  1. 發送 API 請求
    • 我們透過 client.post(URL, json=payload) 來發送請求,請求的 payload 是一段商品描述。
  2. 檢查回應狀態碼
    • assert response.status_code == 200 確保 API 成功處理請求,返回 HTTP 200
  3. 解析 JSON 回應
    • data = response.json() 解析 API 回應,確保它是 JSON 格式
  4. 驗證 API 回應是否包含 summarized_description
    • assert "summarized_description" in data 確保 API 確實產生優化後的描述。
  5. 確認 API 回應包含關鍵欄位
    • API 回應應該包含:
      • title(商品標題)
      • summary(簡要說明)
      • bullet_points(商品特色條列)
    • 這些欄位缺一不可,因此我們使用 assert 來確保它們存在。
  6. 檢查 bullet_points 是否為列表
    • assert isinstance(data["summarized_description"]["bullet_points"], list)
    • bullet_points 是商品特色條列,因此應該是一個 列表(list),這裡的測試確保 API 回應的資料型態正確。

測試異常輸入

在真實環境中,使用者可能會提供不完整的輸入錯誤的格式,因此我們需要測試 API 是否能妥善處理錯誤情境。

📌 測試目標

異常測試主要驗證:

  1. API 是否能正確處理空白輸入
  2. API 是否能應對非 JSON 格式的請求
  3. API 是否能正確回應錯誤資訊

📌 測試程式碼

📍 測試非 JSON 格式輸入

def test_invalid_json():
    """ 測試 API 是否能正確處理非 JSON 格式的請求 """
    response = client.post(URL, content="invalid json")  # 直接傳遞非 JSON 字串

    # FastAPI 內建的 422 Unprocessable Entity 錯誤碼表示請求格式錯誤
    assert response.status_code == 422

📍 測試空白輸入

def test_empty_description():
    """ 測試當輸入為空白時,API 是否能正確處理 """
    payload = {"description": ""}
    response = client.post(URL, json=payload)

    # API 應該回傳 400 Bad Request,因為 description 不能為空
    assert response.status_code == 400

    # 確保 API 回應的錯誤訊息正確
    data = response.json()
    assert data["detail"] == "❌ 商品描述不可為空"

📌 測試解析

非 JSON 格式輸入

  • client.post(URL, content="invalid json"):我們故意傳入非 JSON 格式的字串,而不是 JSON 物件。
  • assert response.status_code == 422:FastAPI 會自動檢查請求格式,如果格式錯誤,會回傳 HTTP 422 Unprocessable Entity

空白輸入

  • payload = {"description": ""}:這裡我們傳遞一個空字串作為 description,來測試 API 是否會正確回應錯誤。
  • assert response.status_code == 400:API 應該回傳 HTTP 400 Bad Request,表示請求無效。
  • assert data["detail"] == "❌ 商品描述不可為空":檢查 API 的錯誤訊息是否正確。

效能測試與優化

測試 API 的請求處理時間

📌 測試目標

在真實環境中,API 可能會面對大量的使用者請求,因此我們需要測試:

  1. API 的請求處理時間:確保 API 在單一請求時的處理時間符合預期。
  2. API 在高併發情境下的效能:模擬多個請求同時發送,觀察 API 是否能穩定運行並保持良好響應速度。
  3. 記錄請求時間,分析效能瓶頸:將測試結果存入紀錄檔,方便後續優化。

📌 測試方法

我們使用 pytest-benchmark 來測量 API 的效能,並透過 ThreadPoolExecutor 來模擬多併發請求

pytest-benchmark

pytest-benchmark 是一個 Python 測試框架插件,用於測試程式執行的效能表現,能夠準確測量單次請求的處理時間

ThreadPoolExecutor

Python 的 concurrent.futures.ThreadPoolExecutor 可用來模擬多個使用者同時發送請求,這有助於評估 API 在高併發環境下的表現。

延伸閱讀:完整解析 ThreadPoolExecutor:Python 高效併發工具

📌 測試程式碼

import pytest
import time
from concurrent.futures import ThreadPoolExecutor
from fastapi.testclient import TestClient
from optimize_description import app

client = TestClient(app)  # 初始化 FastAPI 測試客戶端
URL = "/summarized-description"

@pytest.mark.benchmark
def test_async_api_performance(benchmark):
    """ 測試 API 在單一請求與多併發請求下的效能 """

    payload = {"description": "這款智能手錶具備心率監測與 GPS 追蹤,適合運動健身。"}

    def send_request():
        """ 發送 API 請求並計算執行時間 """
        start_time = time.perf_counter()  # 記錄請求開始時間
        response = client.post(URL, json=payload)  # 發送 API 請求
        end_time = time.perf_counter()  # 記錄請求結束時間

        assert response.status_code == 200  # 確保 API 回應正常
        return end_time - start_time  # 計算請求耗時

    avg_time = 0  # 初始化平均執行時間變數

    try:
        # **測試單一請求的處理時間**
        execution_time = benchmark(send_request)  # 使用 pytest-benchmark 測量單次請求時間

        # **測試多併發請求的效能**
        with ThreadPoolExecutor(max_workers=10) as executor:  # 使用 10 個執行緒模擬 30 個請求
            execution_times = list(executor.map(lambda _: send_request(), range(30)))

        # **計算 30 次請求的平均執行時間**
        avg_time = sum(execution_times) / len(execution_times)

    finally:
        # **將測試結果寫入紀錄檔**
        with open("benchmark.log", "a", encoding="utf-8") as f:
            f.write(f"30 個並發請求的平均執行時間: {avg_time:.4f}\n")

    print(f"🚀 30 個並發請求的平均執行時間: {avg_time:.4f} 秒")

📌 測試解析

🔹 1. 測試單一請求效能
execution_time = benchmark(send_request)
  • 這行程式碼透過 pytest-benchmark 測試單一 API 請求的執行時間,讓我們可以確保個別請求的效能表現穩定
🔹 2. 測試多併發請求效能
with ThreadPoolExecutor(max_workers=10) as executor:
    execution_times = list(executor.map(lambda _: send_request(), range(30)))
  • 我們使用 10 個執行緒 來模擬 30 個請求 同時發送,這可以幫助我們:
    • 檢測 API 是否能夠同時處理多個請求
    • 測試快取機制是否生效
    • 觀察 API 在高負載情境下的穩定性
🔹 3. 記錄測試結果
with open("benchmark.log", "a", encoding="utf-8") as f:
    f.write(f"30 個並發請求的平均執行時間: {avg_time:.4f}\n")
  • 將測試數據寫入紀錄檔,方便後續分析 API 的效能表現。

📌 可能的測試結果與改進方向

測試項目目標可能的問題改進方式
單一請求處理時間應該 < 1 秒超過 1 秒,表示 API 過慢優化 Prompt,減少 Token 使用量
多併發請求穩定性30 個請求應該在合理時間內完成API 超時,併發能力不足使用快取機制減少 OpenAI API 呼叫
併發請求平均時間應該 < 2 秒超過 2 秒,表示 API 無法應對高負載使用非同步處理,減少同步阻塞

API 效能優化

啟用快取

API 請求如果每次都重新計算,會浪費資源。我們可以利用 aiocache 來快取 API 回應:

from aiocache import cached
from aiocache.serializers import JsonSerializer

@cached(ttl=3600, key_builder=lambda func, *args, **kwargs: f"{args[0]}", serializer=JsonSerializer())
async def optimize_description(text: str) -> str:
    ...

這樣,相同輸入的 API 回應可以被快取 1 小時,避免重複呼叫 OpenAI API。

延伸閱讀:高效能快取解決方案——深入解析 AioCache 套件

快取測試

import asyncio
def test_cache_mechanism(monkeypatch):
    asyncio.run(optimize_description.cache.clear())

    call_count = {"calls": 0}

    class FakeChatCompletions:
        def create(self, *args, **kwargs):
            call_count["calls"] += 1
            return {"choices": [{"message": {"content": '{"summarized_description": {"title": "Test", "summary": "Summary", "bullet_points": ["A", "B"]}}'}}]}

    class FakeOpenAI:
        def __init__(self, api_key):
            self.api_key = api_key
            self.chat = FakeChatCompletions()

    monkeypatch.setattr(openai, "OpenAI", FakeOpenAI)

    payload = {"description": "測試快取"}
    
    response1 = client.post(URL, json=payload)
    assert response1.status_code == 200

    response2 = client.post(URL, json=payload)
    assert response2.status_code == 200

    assert call_count["calls"] == 1  # API 只應該被呼叫一次

這段程式碼的主要目的是 測試快取機制,確保當相同的請求發送兩次時,API 只會被呼叫一次,而第二次請求應該從快取中獲取結果。

  • 清空快取 (optimize_description.cache.clear())
  • 使用 monkeypatch 替換 openai.OpenAI,用假 API (FakeOpenAI) 模擬回應
  • 發送相同的 API 請求兩次
  • 檢查 API 是否只被呼叫一次,驗證快取是否生效

限制 Token 使用量

GPT API 計費是基於 Token 計算,因此我們需要:

  • 簡化 Prompt(減少冗長的描述)
  • 設定 max_tokens,避免不必要的回應
response = client.chat.completions.create(  
    model="gpt-4o",
    response_format={"type": "json_object"},
    messages=[{"role": "system", "content": prompt}],
    max_tokens=500  # 限制回應長度
)

總結

本篇文章介紹了:

API 測試策略(基本測試、異常處理)
效能測試方法(基準測試、多併發請求)
效能優化技術(快取、減少 Token、異步處理)

透過這些測試與優化,我們可以確保 API 穩定運行,並且在高併發環境下仍能維持良好的效能 🚀

Similar Posts