Agentic AI项目中的FastAPI应用冒烟测试指南

Agentic AI项目中的FastAPI应用冒烟测试指南

【免费下载链接】learn-agentic-ai Learn Agentic AI using Dapr Agentic Cloud Ascent (DACA) Design Pattern: OpenAI Agents SDK, Memory, MCP, Knowledge Graphs, Docker, Docker Compose, and Kubernetes. 【免费下载链接】learn-agentic-ai 项目地址: https://gitcode.com/GitHub_Trending/le/learn-agentic-ai

概述:为什么冒烟测试在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承担以下关键职责:

mermaid

冒烟测试策略设计

测试金字塔在Agentic AI中的应用

mermaid

核心冒烟测试场景

测试场景测试目标预期结果
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}")

总结与最佳实践

冒烟测试成功的关键指标

mermaid

持续改进策略

  1. 定期评审测试用例:每季度评审冒烟测试用例,确保覆盖核心业务场景
  2. 性能基准监控:建立响应时间基线,监控性能回归
  3. 故障注入测试:模拟依赖服务故障,验证系统韧性
  4. 自动化程度提升:逐步提高测试自动化覆盖率

关键成功因素

  • 早期介入:在开发阶段就建立冒烟测试
  • 持续运行:集成到CI/CD流水线中自动执行
  • 快速反馈:确保测试执行速度快,及时发现问题
  • 业务导向:聚焦核心业务功能验证
  • 文档完善:保持测试用例和文档的同步更新

通过实施本文介绍的冒烟测试策略,您的Agentic AI项目将能够确保FastAPI应用的稳定性和可靠性,为复杂的智能体工作流提供坚实的基础保障。

【免费下载链接】learn-agentic-ai Learn Agentic AI using Dapr Agentic Cloud Ascent (DACA) Design Pattern: OpenAI Agents SDK, Memory, MCP, Knowledge Graphs, Docker, Docker Compose, and Kubernetes. 【免费下载链接】learn-agentic-ai 项目地址: https://gitcode.com/GitHub_Trending/le/learn-agentic-ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值