Agentic AI项目中的FastAPI应用冒烟测试指南
概述:为什么冒烟测试在Agentic AI中至关重要
在Agentic AI(智能体人工智能)项目中,FastAPI作为REST API层承担着关键的角色——它是用户、智能体和微服务之间通信的桥梁。冒烟测试(Smoke Testing)作为最基本的测试类型,确保核心功能在部署后能够正常工作,对于Agentic AI系统的稳定运行至关重要。
冒烟测试的核心目标
- ✅ 验证基础功能:确认API端点能够响应请求
- ✅ 检查依赖集成:验证与OpenAI Agents SDK、Dapr等组件的集成
- ✅ 确保部署成功:在CI/CD流水线中快速反馈部署状态
- ✅ 降低生产风险:在早期发现致命错误
Agentic AI架构中的FastAPI角色
在DACA(Dapr Agentic Cloud Ascent)架构中,FastAPI承担以下关键职责:
冒烟测试策略设计
测试金字塔在Agentic AI中的应用
核心冒烟测试场景
| 测试场景 | 测试目标 | 预期结果 |
|---|---|---|
| API健康检查 | 验证服务可用性 | 200 OK状态码 |
| 基础端点测试 | 验证路由配置 | 正确响应数据 |
| 智能体集成 | 验证AI智能体连接 | 智能响应内容 |
| 工具功能 | 验证工具调用能力 | 工具执行结果 |
| 错误处理 | 验证异常场景处理 | 适当错误响应 |
实战:构建FastAPI冒烟测试套件
环境准备与依赖安装
# 使用uv管理依赖
uv add "fastapi[standard]" pytest pytest-asyncio httpx
uv add openai-agents python-dotenv
# 创建测试目录结构
mkdir -p tests/smoke
基础健康检查测试
# tests/smoke/test_health_check.py
import pytest
from fastapi.testclient import TestClient
from main import app
class TestHealthCheck:
"""基础健康检查冒烟测试"""
@pytest.fixture
def client(self):
return TestClient(app)
def test_root_endpoint(self, client):
"""测试根端点可用性"""
response = client.get("/")
assert response.status_code == 200
assert "message" in response.json()
assert "DACA" in response.json()["message"]
def test_api_docs_available(self, client):
"""验证API文档可访问"""
response = client.get("/docs")
assert response.status_code == 200
assert "swagger" in response.text.lower()
def test_redoc_available(self, client):
"""验证ReDoc文档可访问"""
response = client.get("/redoc")
assert response.status_code == 200
智能体功能冒烟测试
# tests/smoke/test_agent_integration.py
import pytest
from fastapi.testclient import TestClient
from main import app
from unittest.mock import AsyncMock, patch
class TestAgentIntegration:
"""AI智能体集成冒烟测试"""
@pytest.fixture
def client(self):
return TestClient(app)
@pytest.mark.asyncio
async def test_chat_endpoint_smoke(self, client):
"""聊天端点基础冒烟测试"""
with patch("main.Runner.run", new_callable=AsyncMock) as mock_run:
mock_run.return_value.final_output = "测试响应内容"
test_payload = {
"user_id": "test_user",
"text": "Hello, smoke test",
"tags": ["smoke_test"]
}
response = client.post("/chat/", json=test_payload)
# 基础断言
assert response.status_code == 200
assert response.json()["user_id"] == "test_user"
assert "reply" in response.json()
assert "metadata" in response.json()
@pytest.mark.asyncio
async def test_empty_message_handling(self, client):
"""空消息处理冒烟测试"""
test_payload = {
"user_id": "test_user",
"text": "",
"tags": ["smoke_test"]
}
response = client.post("/chat/", json=test_payload)
assert response.status_code == 400
assert "cannot be empty" in response.json()["detail"].lower()
工具功能冒烟测试
# tests/smoke/test_tool_integration.py
import pytest
from fastapi.testclient import TestClient
from main import app, get_current_time
class TestToolIntegration:
"""工具功能冒烟测试"""
def test_time_tool_availability(self):
"""验证时间工具可用性"""
# 直接测试工具函数
time_str = get_current_time()
assert isinstance(time_str, str)
assert len(time_str) > 0
assert "UTC" in time_str
@pytest.mark.asyncio
async def test_tool_in_agent_context(self, client):
"""在智能体上下文中测试工具"""
with patch("main.Runner.run", new_callable=AsyncMock) as mock_run:
mock_run.return_value.final_output = "2025-04-07 12:00:00 UTC"
test_payload = {
"user_id": "test_user",
"text": "What time is it?",
"tags": ["time_query"]
}
response = client.post("/chat/", json=test_payload)
assert response.status_code == 200
assert "2025" in response.json()["reply"]
配置验证测试
# tests/smoke/test_configuration.py
import os
import pytest
from dotenv import load_dotenv
class TestConfiguration:
"""配置验证冒烟测试"""
def test_env_variables_loaded(self):
"""验证环境变量加载"""
load_dotenv()
assert os.getenv("GEMINI_API_KEY") is not None
assert os.getenv("GEMINI_API_KEY") != "your-api-key-here"
def test_required_modules_importable(self):
"""验证必需模块可导入"""
try:
from agents import Agent, Runner, function_tool
from fastapi import FastAPI
from pydantic import BaseModel
assert True
except ImportError as e:
pytest.fail(f"Required module not importable: {e}")
冒烟测试执行与自动化
本地执行命令
# 运行所有冒烟测试
uv run pytest tests/smoke/ -v --tb=short
# 运行特定测试类
uv run pytest tests/smoke/test_health_check.py -v
# 生成测试报告
uv run pytest tests/smoke/ --junitxml=smoke-test-results.xml
CI/CD集成示例
# GitHub Actions 配置示例
name: Smoke Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install UV
run: pip install uv
- name: Install dependencies
run: uv sync --all-groups
- name: Run smoke tests
run: |
uv run pytest tests/smoke/ -v \
--junitxml=test-results/smoke.xml \
--cov=main --cov-report=xml:coverage.xml
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: smoke-test-results
path: test-results/
高级冒烟测试场景
依赖服务健康检查
# tests/smoke/test_dependencies.py
import pytest
import httpx
from fastapi.testclient import TestClient
from main import app
class TestDependencyHealth:
"""依赖服务健康检查"""
@pytest.fixture
def client(self):
return TestClient(app)
@pytest.mark.asyncio
async def test_external_api_connectivity(self):
"""测试外部API连接性"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://generativelanguage.googleapis.com/v1beta/openai/models",
timeout=10.0
)
# 即使返回401(未授权),也说明服务可达
assert response.status_code in [200, 401]
except Exception as e:
pytest.fail(f"External API connectivity failed: {e}")
def test_dapr_sidecar_health(self, client):
"""Dapr sidecar健康检查(如果配置)"""
# 这里可以添加Dapr健康检查端点测试
pass
性能基准冒烟测试
# tests/smoke/test_performance.py
import pytest
import time
from fastapi.testclient import TestClient
from main import app
class TestPerformanceSmoke:
"""性能基准冒烟测试"""
@pytest.fixture
def client(self):
return TestClient(app)
def test_response_time_threshold(self, client):
"""响应时间阈值测试"""
start_time = time.time()
response = client.get("/")
end_time = time.time()
response_time = end_time - start_time
assert response_time < 2.0 # 2秒响应时间阈值
assert response.status_code == 200
@pytest.mark.asyncio
async def test_concurrent_requests_smoke(self, client):
"""并发请求冒烟测试"""
import asyncio
async def make_request():
test_payload = {
"user_id": "load_test_user",
"text": "concurrent test",
"tags": ["load_test"]
}
return client.post("/chat/", json=test_payload)
# 模拟5个并发请求
tasks = [make_request() for _ in range(5)]
responses = await asyncio.gather(*tasks)
# 验证所有请求都成功
for response in responses:
assert response.status_code in [200, 429] # 200成功或429限流
冒烟测试最佳实践
1. 测试数据管理
# tests/conftest.py
import pytest
from unittest.mock import AsyncMock, patch
@pytest.fixture(autouse=True)
def mock_agent_execution():
"""自动mock智能体执行"""
with patch("main.Runner.run", new_callable=AsyncMock) as mock_run:
mock_run.return_value.final_output = "Mocked agent response"
yield mock_run
@pytest.fixture
def sample_chat_payload():
"""标准测试负载"""
return {
"user_id": "test_user",
"text": "Test message for smoke testing",
"metadata": {
"timestamp": "2025-01-01T00:00:00Z",
"session_id": "test-session-123"
},
"tags": ["smoke_test"]
}
2. 错误处理验证
# tests/smoke/test_error_handling.py
import pytest
from fastapi.testclient import TestClient
from main import app
class TestErrorHandling:
"""错误处理冒烟测试"""
@pytest.fixture
def client(self):
return TestClient(app)
def test_invalid_json_payload(self, client):
"""无效JSON负载处理"""
response = client.post("/chat/", data="invalid json")
assert response.status_code == 422
assert "detail" in response.json()
def test_missing_required_fields(self, client):
"""缺失必需字段处理"""
invalid_payload = {"user_id": "test_user"} # 缺少text字段
response = client.post("/chat/", json=invalid_payload)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_agent_timeout_handling(self, client):
"""智能体超时处理"""
with patch("main.Runner.run", new_callable=AsyncMock) as mock_run:
mock_run.side_effect = TimeoutError("Agent execution timeout")
test_payload = {
"user_id": "test_user",
"text": "test message",
"tags": ["smoke_test"]
}
response = client.post("/chat/", json=test_payload)
# 应该返回5xx错误而不是崩溃
assert response.status_code >= 500
3. 安全冒烟测试
# tests/smoke/test_security.py
import pytest
from fastapi.testclient import TestClient
from main import app
class TestSecuritySmoke:
"""安全相关冒烟测试"""
@pytest.fixture
def client(self):
return TestClient(app)
def test_cors_headers_present(self, client):
"""CORS头信息验证"""
response = client.get("/", headers={"Origin": "http://localhost:3000"})
assert "access-control-allow-origin" in response.headers
assert response.headers["access-control-allow-origin"] == "http://localhost:3000"
def test_sensitive_data_exposure(self, client):
"""敏感数据泄露检查"""
response = client.get("/")
response_text = str(response.content)
# 确保不暴露敏感信息
sensitive_patterns = [
"api_key",
"password",
"secret",
"token"
]
for pattern in sensitive_patterns:
assert pattern not in response_text.lower()
冒烟测试报告与监控
测试结果分析
# utils/test_report.py
import json
import xml.etree.ElementTree as ET
from datetime import datetime
def analyze_smoke_test_results(xml_file_path):
"""分析冒烟测试结果"""
tree = ET.parse(xml_file_path)
root = tree.getroot()
results = {
"total_tests": int(root.attrib.get("tests", 0)),
"failures": int(root.attrib.get("failures", 0)),
"errors": int(root.attrib.get("errors", 0)),
"skipped": int(root.attrib.get("skipped", 0)),
"timestamp": datetime.now().isoformat(),
"test_cases": []
}
for testcase in root.findall(".//testcase"):
case = {
"name": testcase.attrib.get("name"),
"classname": testcase.attrib.get("classname"),
"time": float(testcase.attrib.get("time", 0)),
"status": "passed"
}
if testcase.find("failure") is not None:
case["status"] = "failed"
elif testcase.find("error") is not None:
case["status"] = "error"
elif testcase.find("skipped") is not None:
case["status"] = "skipped"
results["test_cases"].append(case)
return results
def generate_smoke_test_report(results):
"""生成冒烟测试报告"""
report = {
"summary": {
"total": results["total_tests"],
"passed": results["total_tests"] - results["failures"] - results["errors"] - results["skipped"],
"failed": results["failures"],
"errors": results["errors"],
"skipped": results["skipped"],
"success_rate": (results["total_tests"] - results["failures"] - results["errors"]) / results["total_tests"] * 100
},
"timestamp": results["timestamp"],
"details": results["test_cases"]
}
return report
监控与告警集成
# monitors/smoke_test_monitor.py
import requests
import logging
from typing import Dict, Any
class SmokeTestMonitor:
"""冒烟测试监控器"""
def __init__(self, base_url: str, alert_webhook: str = None):
self.base_url = base_url
self.alert_webhook = alert_webhook
self.logger = logging.getLogger(__name__)
def run_smoke_test(self) -> Dict[str, Any]:
"""运行冒烟测试并返回结果"""
test_cases = [
self._test_health_endpoint,
self._test_docs_endpoint,
self._test_chat_endpoint
]
results = {
"timestamp": datetime.now().isoformat(),
"tests": [],
"overall_status": "pass"
}
for test_func in test_cases:
try:
result = test_func()
results["tests"].append(result)
if result["status"] != "pass":
results["overall_status"] = "fail"
except Exception as e:
self.logger.error(f"Smoke test {test_func.__name__} failed: {e}")
results["tests"].append({
"name": test_func.__name__,
"status": "error",
"error": str(e)
})
results["overall_status"] = "fail"
if results["overall_status"] == "fail" and self.alert_webhook:
self._send_alert(results)
return results
def _test_health_endpoint(self) -> Dict[str, Any]:
"""测试健康端点"""
response = requests.get(f"{self.base_url}/", timeout=10)
return {
"name": "health_endpoint",
"status": "pass" if response.status_code == 200 else "fail",
"response_time": response.elapsed.total_seconds(),
"status_code": response.status_code
}
def _send_alert(self, results: Dict[str, Any]):
"""发送告警"""
if self.alert_webhook:
try:
requests.post(self.alert_webhook, json=results, timeout=5)
except Exception as e:
self.logger.error(f"Failed to send alert: {e}")
总结与最佳实践
冒烟测试成功的关键指标
持续改进策略
- 定期评审测试用例:每季度评审冒烟测试用例,确保覆盖核心业务场景
- 性能基准监控:建立响应时间基线,监控性能回归
- 故障注入测试:模拟依赖服务故障,验证系统韧性
- 自动化程度提升:逐步提高测试自动化覆盖率
关键成功因素
- ✅ 早期介入:在开发阶段就建立冒烟测试
- ✅ 持续运行:集成到CI/CD流水线中自动执行
- ✅ 快速反馈:确保测试执行速度快,及时发现问题
- ✅ 业务导向:聚焦核心业务功能验证
- ✅ 文档完善:保持测试用例和文档的同步更新
通过实施本文介绍的冒烟测试策略,您的Agentic AI项目将能够确保FastAPI应用的稳定性和可靠性,为复杂的智能体工作流提供坚实的基础保障。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



