API 自動化測試與效能優化
更新日期: 2025 年 2 月 13 日
本文為 AI 描述優化 api 設計 系列文,第 12 篇:
- 如何設計一個商品描述優化 API?—— 完整指南
- 設計 AI 優化商品描述的 Prompt
- FastAPI:高效且易用的 Python Web 框架
- 介紹 Uvicorn:高效能 ASGI 伺服器
- Uvicorn 監聽 0.0.0.0,但為何 API 只能用 127.0.0.1 訪問?
- Tenacity:強大的 Python 重試機制庫
- FastAPI 建立商品描述優化 API
- 介紹 pytest:Python 測試框架的強大選擇
- Python httpx 完整指南:高效的 HTTP 客戶端
- Pytest-Benchmark:高效能測試的基準分析工具
- 深入解析 monkeypatch:Python 測試中的強大工具
- API 自動化測試與效能優化 👈進度
建議閱讀本文前,先具備 聊天機器人-建議回復 相關系列文的概念。
在開發「商品描述優化 API」時,確保 API 穩定、高效運行至關重要。
透過 自動化測試,我們可以驗證 API 的輸出是否正確,並確保其能夠應對各種異常情境。
而 效能優化 則能提升 API 的處理速度,減少資源消耗,讓 API 能夠應對更大規模的請求量。
API 自動化測試
為什麼要進行 API 測試?
API 測試的主要目標是:
- 確保 API 輸出符合預期(如 JSON 格式正確、關鍵欄位存在)
- 驗證 API 是否能正確處理異常輸入(如空值、錯誤格式)
- 測試 API 效能,確保在高併發情境下仍能穩定運行
我們使用 pytest
和 FastAPI 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)
📌 測試解析
這段測試程式碼的流程如下:
- 發送 API 請求
- 我們透過
client.post(URL, json=payload)
來發送請求,請求的payload
是一段商品描述。
- 我們透過
- 檢查回應狀態碼
assert response.status_code == 200
確保 API 成功處理請求,返回 HTTP 200。
- 解析 JSON 回應
data = response.json()
解析 API 回應,確保它是 JSON 格式。
- 驗證 API 回應是否包含
summarized_description
assert "summarized_description" in data
確保 API 確實產生優化後的描述。
- 確認 API 回應包含關鍵欄位
- API 回應應該包含:
title
(商品標題)summary
(簡要說明)bullet_points
(商品特色條列)
- 這些欄位缺一不可,因此我們使用
assert
來確保它們存在。
- API 回應應該包含:
- 檢查
bullet_points
是否為列表assert isinstance(data["summarized_description"]["bullet_points"], list)
bullet_points
是商品特色條列,因此應該是一個 列表(list),這裡的測試確保 API 回應的資料型態正確。
測試異常輸入
在真實環境中,使用者可能會提供不完整的輸入或錯誤的格式,因此我們需要測試 API 是否能妥善處理錯誤情境。
📌 測試目標
異常測試主要驗證:
- API 是否能正確處理空白輸入
- API 是否能應對非 JSON 格式的請求
- 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 可能會面對大量的使用者請求,因此我們需要測試:
- API 的請求處理時間:確保 API 在單一請求時的處理時間符合預期。
- API 在高併發情境下的效能:模擬多個請求同時發送,觀察 API 是否能穩定運行並保持良好響應速度。
- 記錄請求時間,分析效能瓶頸:將測試結果存入紀錄檔,方便後續優化。
📌 測試方法
我們使用 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 穩定運行,並且在高併發環境下仍能維持良好的效能 🚀