【剪映小助手源码精讲】37_测试体系构建

22 阅读33分钟

第37章:测试体系构建

37.1 概述

测试体系是确保软件质量和可靠性的重要保障。剪映小助手构建了一套完整的测试体系,涵盖单元测试、集成测试、API接口测试、业务逻辑测试、异常场景测试和性能测试等多个层面。通过系统化的测试策略和自动化的测试流程,确保系统的每个组件都能稳定可靠地运行。

本章将深入剖析测试体系的架构设计、测试框架的搭建过程、各类测试用例的实现细节,以及测试自动化的最佳实践。通过实际的测试代码示例,展示如何构建一个专业、高效的测试体系。

37.2 测试框架架构设计

37.2.1 测试框架整体架构

剪映小助手采用pytest作为核心测试框架,结合FastAPI的测试客户端、HTTP请求库等工具,构建了一个功能完善的测试体系:

# tests/conftest.py
import pytest
import asyncio
from typing import Generator
from fastapi.testclient import TestClient
from src.main import app
from src.config import settings

@pytest.fixture(scope="session")
def event_loop() -> Generator:
    """创建事件循环"""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="session")
def test_client() -> Generator:
    """创建测试客户端"""
    with TestClient(app) as client:
        yield client

@pytest.fixture(scope="session")
def test_settings():
    """测试环境配置"""
    # 使用测试专用配置
    settings.DRAFT_DIR = "./test_drafts"
    settings.LOG_LEVEL = "DEBUG"
    settings.DEBUG = True
    return settings

@pytest.fixture(autouse=True)
def setup_test_environment():
    """测试环境初始化"""
    # 清理测试数据
    import shutil
    import os
    
    test_dirs = ["./test_drafts", "./test_outputs", "./test_cache"]
    for test_dir in test_dirs:
        if os.path.exists(test_dir):
            shutil.rmtree(test_dir)
        os.makedirs(test_dir, exist_ok=True)
    
    yield
    
    # 测试后清理
    for test_dir in test_dirs:
        if os.path.exists(test_dir):
            shutil.rmtree(test_dir)

37.2.2 测试配置管理

系统提供了灵活的测试配置管理,支持不同环境的测试需求:

# tests/config.py
import os
from typing import Dict, Any
from dataclasses import dataclass

@dataclass
class TestConfig:
    """测试配置"""
    # 测试服务器配置
    TEST_SERVER_HOST: str = "localhost"
    TEST_SERVER_PORT: int = 8000
    TEST_API_BASE_URL: str = f"http://{TEST_SERVER_HOST}:{TEST_SERVER_PORT}"
    
    # 测试数据配置
    TEST_DATA_DIR: str = "./tests/data"
    TEST_OUTPUT_DIR: str = "./tests/outputs"
    
    # 测试超时配置
    TEST_TIMEOUT: int = 30
    API_REQUEST_TIMEOUT: int = 10
    
    # 重试配置
    MAX_RETRY_ATTEMPTS: int = 3
    RETRY_DELAY: float = 1.0
    
    # 性能测试配置
    PERFORMANCE_TEST_USERS: int = 10
    PERFORMANCE_TEST_DURATION: int = 60
    
    def get_test_config(self) -> Dict[str, Any]:
        """获取测试配置"""
        return {
            key: value for key, value in self.__dict__.items()
            if not key.startswith('_')
        }

# 全局测试配置实例
test_config = TestConfig()

37.2.3 测试工具函数

系统提供了丰富的测试工具函数,简化测试用例的编写:

# tests/utils/test_helpers.py
import json
import time
import requests
from typing import Dict, Any, Optional
from tests.config import test_config

class TestHelpers:
    """测试辅助工具类"""
    
    @staticmethod
    def create_test_draft_data(**kwargs) -> Dict[str, Any]:
        """创建测试草稿数据"""
        default_data = {
            "draft_name": "test_draft",
            "resolution": "1080p",
            "duration": 60,
            "materials": [],
            "effects": [],
            "transitions": []
        }
        default_data.update(kwargs)
        return default_data
    
    @staticmethod
    def create_test_material_data(material_type: str = "video", **kwargs) -> Dict[str, Any]:
        """创建测试素材数据"""
        default_data = {
            "type": material_type,
            "url": f"https://example.com/test.{material_type}",
            "duration": 10,
            "start_time": 0,
            "end_time": 10
        }
        default_data.update(kwargs)
        return default_data
    
    @staticmethod
    def make_api_request(
        method: str,
        endpoint: str,
        data: Optional[Dict] = None,
        expected_status: int = 200,
        timeout: int = None
    ) -> requests.Response:
        """发送API请求"""
        url = f"{test_config.TEST_API_BASE_URL}{endpoint}"
        timeout = timeout or test_config.API_REQUEST_TIMEOUT
        
        try:
            if method.upper() == "GET":
                response = requests.get(url, params=data, timeout=timeout)
            elif method.upper() == "POST":
                response = requests.post(url, json=data, timeout=timeout)
            elif method.upper() == "PUT":
                response = requests.put(url, json=data, timeout=timeout)
            elif method.upper() == "DELETE":
                response = requests.delete(url, timeout=timeout)
            else:
                raise ValueError(f"不支持的HTTP方法: {method}")
            
            assert response.status_code == expected_status, \
                f"期望状态码 {expected_status},实际状态码 {response.status_code}"
            
            return response
            
        except requests.exceptions.RequestException as e:
            raise AssertionError(f"API请求失败: {str(e)}")
    
    @staticmethod
    def retry_on_failure(func, max_attempts: int = None, delay: float = None):
        """失败重试装饰器"""
        max_attempts = max_attempts or test_config.MAX_RETRY_ATTEMPTS
        delay = delay or test_config.RETRY_DELAY
        
        for attempt in range(max_attempts):
            try:
                return func()
            except Exception as e:
                if attempt == max_attempts - 1:
                    raise
                time.sleep(delay * (2 ** attempt))  # 指数退避
    
    @staticmethod
    def assert_response_format(response_data: Dict[str, Any], expected_keys: list):
        """验证响应格式"""
        for key in expected_keys:
            assert key in response_data, f"响应中缺少必需的键: {key}"
    
    @staticmethod
    def assert_error_response(response_data: Dict[str, Any], expected_error_code: int):
        """验证错误响应"""
        assert response_data.get("success") is False, "错误响应的success字段应为False"
        assert "error" in response_data, "错误响应应包含error字段"
        
        error_data = response_data["error"]
        assert error_data.get("code") == expected_error_code, \
            f"期望错误码 {expected_error_code},实际错误码 {error_data.get('code')}"
        assert "message" in error_data, "错误信息应包含message字段"

37.3 API接口测试

37.3.1 基础API测试

API接口测试是测试体系的核心部分,确保所有接口都能正常工作:

# tests/test_api_basic.py
import pytest
import requests
from fastapi.testclient import TestClient
from tests.utils.test_helpers import TestHelpers
from tests.config import test_config

class TestAPIBasic:
    """基础API测试类"""
    
    def test_health_check(self, test_client: TestClient):
        """测试健康检查接口"""
        response = test_client.get("/health")
        assert response.status_code == 200
        
        data = response.json()
        assert data["status"] == "healthy"
        assert "timestamp" in data
    
    def test_api_version(self, test_client: TestClient):
        """测试API版本接口"""
        response = test_client.get("/api/version")
        assert response.status_code == 200
        
        data = response.json()
        assert "version" in data
        assert "api_version" in data
    
    def test_invalid_endpoint(self, test_client: TestClient):
        """测试无效端点"""
        response = test_client.get("/invalid/endpoint")
        assert response.status_code == 404
        
        data = response.json()
        TestHelpers.assert_error_response(data, 4040)  # 假设的错误码
    
    def test_method_not_allowed(self, test_client: TestClient):
        """测试不允许的HTTP方法"""
        response = test_client.delete("/health")
        assert response.status_code == 405
        
        data = response.json()
        TestHelpers.assert_error_response(data, 4050)  # 假设的错误码

37.3.2 草稿管理API测试

草稿管理是核心功能,需要进行全面的API测试:

# tests/test_draft_api.py
import pytest
import json
from fastapi.testclient import TestClient
from tests.utils.test_helpers import TestHelpers

class TestDraftAPI:
    """草稿管理API测试类"""
    
    def test_create_draft_success(self, test_client: TestClient):
        """测试成功创建草稿"""
        draft_data = TestHelpers.create_test_draft_data(
            draft_name="test_create_draft",
            resolution="1080p"
        )
        
        response = test_client.post("/v1/draft/create", json=draft_data)
        assert response.status_code == 201
        
        data = response.json()
        assert data["success"] is True
        assert "draft_id" in data["data"]
        assert data["data"]["draft_name"] == draft_data["draft_name"]
    
    def test_create_draft_missing_required_params(self, test_client: TestClient):
        """测试创建草稿缺少必填参数"""
        invalid_data = {"resolution": "1080p"}  # 缺少draft_name
        
        response = test_client.post("/v1/draft/create", json=invalid_data)
        assert response.status_code == 400
        
        data = response.json()
        TestHelpers.assert_error_response(data, 1001)  # 缺少必填参数
    
    def test_create_draft_invalid_resolution(self, test_client: TestClient):
        """测试创建草稿无效分辨率"""
        draft_data = TestHelpers.create_test_draft_data(
            resolution="invalid_resolution"
        )
        
        response = test_client.post("/v1/draft/create", json=draft_data)
        assert response.status_code == 400
        
        data = response.json()
        TestHelpers.assert_error_response(data, 1003)  # 参数范围错误
    
    def test_get_draft_success(self, test_client: TestClient):
        """测试成功获取草稿"""
        # 首先创建一个草稿
        draft_data = TestHelpers.create_test_draft_data()
        create_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = create_response.json()["data"]["draft_id"]
        
        # 然后获取草稿
        response = test_client.get(f"/v1/draft/{draft_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["draft_id"] == draft_id
    
    def test_get_draft_not_found(self, test_client: TestClient):
        """测试获取不存在的草稿"""
        invalid_draft_id = "nonexistent_draft_id"
        
        response = test_client.get(f"/v1/draft/{invalid_draft_id}")
        assert response.status_code == 404
        
        data = response.json()
        TestHelpers.assert_error_response(data, 2000)  # 草稿不存在
    
    def test_update_draft_success(self, test_client: TestClient):
        """测试成功更新草稿"""
        # 首先创建一个草稿
        draft_data = TestHelpers.create_test_draft_data()
        create_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = create_response.json()["data"]["draft_id"]
        
        # 更新草稿
        update_data = {"resolution": "4K", "duration": 120}
        response = test_client.put(f"/v1/draft/{draft_id}", json=update_data)
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["resolution"] == "4K"
        assert data["data"]["duration"] == 120
    
    def test_delete_draft_success(self, test_client: TestClient):
        """测试成功删除草稿"""
        # 首先创建一个草稿
        draft_data = TestHelpers.create_test_draft_data()
        create_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = create_response.json()["data"]["draft_id"]
        
        # 删除草稿
        response = test_client.delete(f"/v1/draft/{draft_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        
        # 验证草稿已被删除
        get_response = test_client.get(f"/v1/draft/{draft_id}")
        assert get_response.status_code == 404

37.3.3 素材管理API测试

素材管理是视频编辑的重要功能,需要进行全面的测试:

# tests/test_material_api.py
import pytest
import base64
from fastapi.testclient import TestClient
from tests.utils.test_helpers import TestHelpers

class TestMaterialAPI:
    """素材管理API测试类"""
    
    def test_add_image_material_success(self, test_client: TestClient):
        """测试成功添加图片素材"""
        # 创建测试图片数据
        test_image_data = base64.b64encode(b"fake_image_data").decode()
        
        material_data = {
            "type": "image",
            "data": test_image_data,
            "filename": "test_image.jpg",
            "duration": 5
        }
        
        response = test_client.post("/v1/material/add", json=material_data)
        assert response.status_code == 201
        
        data = response.json()
        assert data["success"] is True
        assert "material_id" in data["data"]
    
    def test_add_audio_material_success(self, test_client: TestClient):
        """测试成功添加音频素材"""
        # 创建测试音频数据
        test_audio_data = base64.b64encode(b"fake_audio_data").decode()
        
        material_data = {
            "type": "audio",
            "data": test_audio_data,
            "filename": "test_audio.mp3",
            "duration": 30
        }
        
        response = test_client.post("/v1/material/add", json=material_data)
        assert response.status_code == 201
        
        data = response.json()
        assert data["success"] is True
        assert "material_id" in data["data"]
    
    def test_add_material_invalid_type(self, test_client: TestClient):
        """测试添加无效类型的素材"""
        material_data = {
            "type": "invalid_type",
            "data": "test_data",
            "filename": "test.file"
        }
        
        response = test_client.post("/v1/material/add", json=material_data)
        assert response.status_code == 400
        
        data = response.json()
        TestHelpers.assert_error_response(data, 2003)  # 素材格式不支持
    
    def test_add_material_missing_data(self, test_client: TestClient):
        """测试添加素材缺少数据"""
        material_data = {
            "type": "image",
            "filename": "test_image.jpg"
            # 缺少data字段
        }
        
        response = test_client.post("/v1/material/add", json=material_data)
        assert response.status_code == 400
        
        data = response.json()
        TestHelpers.assert_error_response(data, 1001)  # 缺少必填参数
    
    def test_get_material_success(self, test_client: TestClient):
        """测试成功获取素材"""
        # 首先添加一个素材
        test_image_data = base64.b64encode(b"fake_image_data").decode()
        material_data = {
            "type": "image",
            "data": test_image_data,
            "filename": "test_image.jpg",
            "duration": 5
        }
        
        add_response = test_client.post("/v1/material/add", json=material_data)
        material_id = add_response.json()["data"]["material_id"]
        
        # 然后获取素材
        response = test_client.get(f"/v1/material/{material_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["material_id"] == material_id
        assert data["data"]["type"] == "image"
    
    def test_get_material_not_found(self, test_client: TestClient):
        """测试获取不存在的素材"""
        invalid_material_id = "nonexistent_material_id"
        
        response = test_client.get(f"/v1/material/{invalid_material_id}")
        assert response.status_code == 404
        
        data = response.json()
        TestHelpers.assert_error_response(data, 2002)  # 素材不存在
    
    def test_delete_material_success(self, test_client: TestClient):
        """测试成功删除素材"""
        # 首先添加一个素材
        test_image_data = base64.b64encode(b"fake_image_data").decode()
        material_data = {
            "type": "image",
            "data": test_image_data,
            "filename": "test_image.jpg",
            "duration": 5
        }
        
        add_response = test_client.post("/v1/material/add", json=material_data)
        material_id = add_response.json()["data"]["material_id"]
        
        # 删除素材
        response = test_client.delete(f"/v1/material/{material_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        
        # 验证素材已被删除
        get_response = test_client.get(f"/v1/material/{material_id}")
        assert get_response.status_code == 404

37.3.4 视频生成API测试

视频生成是核心功能,需要进行全面的测试:

# tests/test_video_generation_api.py
import pytest
import time
from fastapi.testclient import TestClient
from tests.utils.test_helpers import TestHelpers

class TestVideoGenerationAPI:
    """视频生成API测试类"""
    
    def test_submit_video_generation_success(self, test_client: TestClient):
        """测试成功提交视频生成任务"""
        # 首先创建草稿
        draft_data = TestHelpers.create_test_draft_data()
        draft_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = draft_response.json()["data"]["draft_id"]
        
        # 提交视频生成任务
        generation_data = {
            "draft_id": draft_id,
            "output_format": "mp4",
            "quality": "high",
            "resolution": "1080p"
        }
        
        response = test_client.post("/v1/video/generate", json=generation_data)
        assert response.status_code == 202  # 已接受,异步处理
        
        data = response.json()
        assert data["success"] is True
        assert "task_id" in data["data"]
        assert data["data"]["status"] == "pending"
    
    def test_submit_video_generation_invalid_draft(self, test_client: TestClient):
        """测试提交无效草稿的视频生成任务"""
        generation_data = {
            "draft_id": "invalid_draft_id",
            "output_format": "mp4",
            "quality": "high"
        }
        
        response = test_client.post("/v1/video/generate", json=generation_data)
        assert response.status_code == 404
        
        data = response.json()
        TestHelpers.assert_error_response(data, 2000)  # 草稿不存在
    
    def test_get_video_generation_status(self, test_client: TestClient):
        """测试获取视频生成状态"""
        # 首先创建草稿和生成任务
        draft_data = TestHelpers.create_test_draft_data()
        draft_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = draft_response.json()["data"]["draft_id"]
        
        generation_data = {"draft_id": draft_id, "output_format": "mp4"}
        submit_response = test_client.post("/v1/video/generate", json=generation_data)
        task_id = submit_response.json()["data"]["task_id"]
        
        # 获取任务状态
        response = test_client.get(f"/v1/video/status/{task_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["task_id"] == task_id
        assert "status" in data["data"]
    
    def test_get_video_generation_result(self, test_client: TestClient):
        """测试获取视频生成结果"""
        # 首先创建草稿和生成任务
        draft_data = TestHelpers.create_test_draft_data()
        draft_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = draft_response.json()["data"]["draft_id"]
        
        generation_data = {"draft_id": draft_id, "output_format": "mp4"}
        submit_response = test_client.post("/v1/video/generate", json=generation_data)
        task_id = submit_response.json()["data"]["task_id"]
        
        # 等待任务完成(简化测试,实际应该使用轮询或回调)
        time.sleep(2)
        
        # 获取生成结果
        response = test_client.get(f"/v1/video/result/{task_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["task_id"] == task_id
        assert "video_url" in data["data"] or data["data"]["status"] != "completed"
    
    def test_cancel_video_generation(self, test_client: TestClient):
        """测试取消视频生成任务"""
        # 首先创建草稿和生成任务
        draft_data = TestHelpers.create_test_draft_data()
        draft_response = test_client.post("/v1/draft/create", json=draft_data)
        draft_id = draft_response.json()["data"]["draft_id"]
        
        generation_data = {"draft_id": draft_id, "output_format": "mp4"}
        submit_response = test_client.post("/v1/video/generate", json=generation_data)
        task_id = submit_response.json()["data"]["task_id"]
        
        # 取消任务
        response = test_client.post(f"/v1/video/cancel/{task_id}")
        assert response.status_code == 200
        
        data = response.json()
        assert data["success"] is True
        assert data["data"]["status"] == "cancelled"

37.4 业务逻辑测试

37.4.1 草稿服务业务逻辑测试

业务逻辑测试关注服务的核心业务逻辑是否正确实现:

# tests/test_draft_service_logic.py
import pytest
from unittest.mock import Mock, patch
from src.service.draft_service import DraftService
from src.exceptions import CustomException, CustomError
from tests.utils.test_helpers import TestHelpers

class TestDraftServiceLogic:
    """草稿服务业务逻辑测试类"""
    
    def setup_method(self):
        """测试方法前置设置"""
        self.draft_service = DraftService()
        self.test_draft_data = TestHelpers.create_test_draft_data()
    
    def test_create_draft_validation_success(self):
        """测试创建草稿参数验证成功"""
        # 测试有效的草稿数据
        result = self.draft_service.validate_draft_data(self.test_draft_data)
        assert result is True
    
    def test_create_draft_validation_missing_name(self):
        """测试创建草稿缺少名称验证"""
        invalid_data = self.test_draft_data.copy()
        del invalid_data["draft_name"]
        
        with pytest.raises(CustomException) as exc_info:
            self.draft_service.validate_draft_data(invalid_data)
        
        assert exc_info.value.error_code == CustomError.MISSING_REQUIRED_PARAMETER
        assert "draft_name" in exc_info.value.message
    
    def test_create_draft_validation_invalid_resolution(self):
        """测试创建草稿无效分辨率验证"""
        invalid_data = self.test_draft_data.copy()
        invalid_data["resolution"] = "invalid_resolution"
        
        with pytest.raises(CustomException) as exc_info:
            self.draft_service.validate_draft_data(invalid_data)
        
        assert exc_info.value.error_code == CustomError.PARAMETER_RANGE_ERROR
        assert "resolution" in exc_info.value.message
    
    @patch('src.service.draft_service.os.path.exists')
    def test_draft_exists_check(self, mock_exists):
        """测试草稿存在性检查"""
        # 模拟草稿存在
        mock_exists.return_value = True
        result = self.draft_service.check_draft_exists("test_draft_path")
        assert result is True
        
        # 模拟草稿不存在
        mock_exists.return_value = False
        result = self.draft_service.check_draft_exists("test_draft_path")
        assert result is False
    
    @patch('src.service.draft_service.os.makedirs')
    @patch('src.service.draft_service.os.path.exists')
    def test_create_draft_directory(self, mock_exists, mock_makedirs):
        """测试创建草稿目录"""
        # 模拟目录已存在
        mock_exists.return_value = True
        result = self.draft_service.create_draft_directory("test_draft_dir")
        assert result is True
        mock_makedirs.assert_not_called()
        
        # 模拟目录不存在
        mock_exists.return_value = False
        result = self.draft_service.create_draft_directory("test_draft_dir")
        assert result is True
        mock_makedirs.assert_called_once()
    
    def test_draft_data_serialization(self):
        """测试草稿数据序列化"""
        serialized_data = self.draft_service.serialize_draft_data(self.test_draft_data)
        
        assert isinstance(serialized_data, str)
        assert "draft_name" in serialized_data
        assert "resolution" in serialized_data
    
    def test_draft_data_deserialization(self):
        """测试草稿数据反序列化"""
        # 首先序列化数据
        serialized_data = self.draft_service.serialize_draft_data(self.test_draft_data)
        
        # 然后反序列化
        deserialized_data = self.draft_service.deserialize_draft_data(serialized_data)
        
        assert deserialized_data["draft_name"] == self.test_draft_data["draft_name"]
        assert deserialized_data["resolution"] == self.test_draft_data["resolution"]
    
    @patch('src.service.draft_service.json.dump')
    @patch('builtins.open', create=True)
    def test_save_draft_to_file(self, mock_open, mock_json_dump):
        """测试保存草稿到文件"""
        result = self.draft_service.save_draft_to_file(
            self.test_draft_data, 
            "test_draft_path.json"
        )
        
        assert result is True
        mock_open.assert_called_once_with("test_draft_path.json", 'w', encoding='utf-8')
        mock_json_dump.assert_called_once()
    
    def test_draft_metadata_extraction(self):
        """测试草稿元数据提取"""
        metadata = self.draft_service.extract_draft_metadata(self.test_draft_data)
        
        assert "draft_name" in metadata
        assert "resolution" in metadata
        assert "duration" in metadata
        assert "created_at" in metadata

37.4.2 素材处理业务逻辑测试

素材处理涉及多种文件格式和处理逻辑,需要进行详细测试:

# tests/test_material_service_logic.py
import pytest
from unittest.mock import Mock, patch, MagicMock
from src.service.material_service import MaterialService
from src.exceptions import CustomException, CustomError
from tests.utils.test_helpers import TestHelpers

class TestMaterialServiceLogic:
    """素材服务业务逻辑测试类"""
    
    def setup_method(self):
        """测试方法前置设置"""
        self.material_service = MaterialService()
        self.test_material_data = TestHelpers.create_test_material_data()
    
    def test_validate_material_type_success(self):
        """测试素材类型验证成功"""
        valid_types = ["video", "audio", "image", "text"]
        
        for material_type in valid_types:
            result = self.material_service.validate_material_type(material_type)
            assert result is True
    
    def test_validate_material_type_invalid(self):
        """测试无效素材类型验证"""
        invalid_types = ["invalid", "document", "", None]
        
        for material_type in invalid_types:
            with pytest.raises(CustomException) as exc_info:
                self.material_service.validate_material_type(material_type)
            
            assert exc_info.value.error_code == CustomError.MATERIAL_FORMAT_NOT_SUPPORTED
    
    @patch('src.service.material_service.mimetypes.guess_type')
    def test_validate_material_format_by_filename(self, mock_guess_type):
        """测试通过文件名验证素材格式"""
        # 模拟有效的视频格式
        mock_guess_type.return_value = ("video/mp4", None)
        result = self.material_service.validate_material_format("test.mp4")
        assert result is True
        
        # 模拟无效格式
        mock_guess_type.return_value = ("application/pdf", None)
        with pytest.raises(CustomException) as exc_info:
            self.material_service.validate_material_format("test.pdf")
        
        assert exc_info.value.error_code == CustomError.MATERIAL_FORMAT_NOT_SUPPORTED
    
    def test_validate_material_size(self):
        """测试素材大小验证"""
        # 测试有效大小(小于100MB)
        result = self.material_service.validate_material_size(50 * 1024 * 1024)  # 50MB
        assert result is True
        
        # 测试过大文件
        with pytest.raises(CustomException) as exc_info:
            self.material_service.validate_material_size(150 * 1024 * 1024)  # 150MB
        
        assert exc_info.value.error_code == CustomError.MATERIAL_SIZE_EXCEEDED
    
    @patch('src.service.material_service.base64.b64decode')
    def test_decode_base64_material(self, mock_b64decode):
        """测试Base64解码素材数据"""
        test_data = "dGVzdF9kYXRh"  # "test_data"的Base64编码
        mock_b64decode.return_value = b"test_data"
        
        result = self.material_service.decode_base64_material(test_data)
        assert result == b"test_data"
        mock_b64decode.assert_called_once_with(test_data)
    
    @patch('builtins.open')
    @patch('src.service.material_service.os.makedirs')
    def test_save_material_to_file(self, mock_makedirs, mock_open):
        """测试保存素材到文件"""
        test_data = b"test_material_data"
        file_path = "/test/materials/test_material.mp4"
        
        result = self.material_service.save_material_to_file(test_data, file_path)
        
        assert result is True
        mock_makedirs.assert_called_once()
        mock_open.assert_called_once_with(file_path, 'wb')
    
    @patch('src.service.material_service.Image.open')
    def test_process_image_material(self, mock_image_open):
        """测试处理图片素材"""
        mock_image = Mock()
        mock_image.size = (1920, 1080)
        mock_image.format = "JPEG"
        mock_image_open.return_value = mock_image
        
        file_path = "/test/image.jpg"
        result = self.material_service.process_image_material(file_path)
        
        assert result["width"] == 1920
        assert result["height"] == 1080
        assert result["format"] == "JPEG"
        mock_image_open.assert_called_once_with(file_path)
    
    @patch('src.service.material_service.mutagen.File')
    def test_process_audio_material(self, mock_mutagen_file):
        """测试处理音频素材"""
        mock_audio = Mock()
        mock_audio.info.length = 180  # 3分钟
        mock_audio.info.bitrate = 320000  # 320kbps
        mock_mutagen_file.return_value = mock_audio
        
        file_path = "/test/audio.mp3"
        result = self.material_service.process_audio_material(file_path)
        
        assert result["duration"] == 180
        assert result["bitrate"] == 320000
        mock_mutagen_file.assert_called_once_with(file_path)
    
    def test_calculate_material_hash(self):
        """测试计算素材哈希值"""
        test_data = b"test_material_content"
        
        hash1 = self.material_service.calculate_material_hash(test_data)
        hash2 = self.material_service.calculate_material_hash(test_data)
        
        assert hash1 == hash2  # 相同内容应该产生相同的哈希值
        assert len(hash1) == 64  # SHA256哈希值长度
    
    def test_check_material_duplicate(self):
        """测试检查素材重复"""
        test_hash = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
        
        # 模拟数据库查询
        with patch.object(self.material_service, 'get_material_by_hash') as mock_get:
            # 模拟素材已存在
            mock_get.return_value = {"material_id": "existing_material_id"}
            result = self.material_service.check_material_duplicate(test_hash)
            assert result is True
            
            # 模拟素材不存在
            mock_get.return_value = None
            result = self.material_service.check_material_duplicate(test_hash)
            assert result is False

37.4.3 视频生成业务逻辑测试

视频生成涉及复杂的处理流程,需要详细测试每个环节:

# tests/test_video_generation_logic.py
import pytest
from unittest.mock import Mock, patch, MagicMock
from src.service.video_generation_service import VideoGenerationService
from src.exceptions import CustomException, CustomError
from tests.utils.test_helpers import TestHelpers

class TestVideoGenerationLogic:
    """视频生成业务逻辑测试类"""
    
    def setup_method(self):
        """测试方法前置设置"""
        self.video_service = VideoGenerationService()
        self.test_draft_data = TestHelpers.create_test_draft_data()
    
    def test_validate_generation_params_success(self):
        """测试生成参数验证成功"""
        valid_params = {
            "draft_id": "test_draft_id",
            "output_format": "mp4",
            "quality": "high",
            "resolution": "1080p"
        }
        
        result = self.video_service.validate_generation_params(valid_params)
        assert result is True
    
    def test_validate_generation_params_missing_required(self):
        """测试生成参数缺少必填项"""
        invalid_params = {
            "output_format": "mp4",
            "quality": "high"
            # 缺少draft_id
        }
        
        with pytest.raises(CustomException) as exc_info:
            self.video_service.validate_generation_params(invalid_params)
        
        assert exc_info.value.error_code == CustomError.MISSING_REQUIRED_PARAMETER
    
    def test_validate_generation_params_invalid_format(self):
        """测试生成参数无效格式"""
        invalid_params = {
            "draft_id": "test_draft_id",
            "output_format": "invalid_format",
            "quality": "high"
        }
        
        with pytest.raises(CustomException) as exc_info:
            self.video_service.validate_generation_params(invalid_params)
        
        assert exc_info.value.error_code == CustomError.PARAMETER_RANGE_ERROR
    
    def test_validate_generation_params_invalid_quality(self):
        """测试生成参数无效质量设置"""
        invalid_params = {
            "draft_id": "test_draft_id",
            "output_format": "mp4",
            "quality": "invalid_quality"
        }
        
        with pytest.raises(CustomException) as exc_info:
            self.video_service.validate_generation_params(invalid_params)
        
        assert exc_info.value.error_code == CustomError.PARAMETER_RANGE_ERROR
    
    def test_generate_task_id(self):
        """测试生成任务ID"""
        task_id1 = self.video_service.generate_task_id("test_draft_id")
        task_id2 = self.video_service.generate_task_id("test_draft_id")
        
        assert task_id1 != task_id2  # 应该生成不同的任务ID
        assert len(task_id1) == 32  # MD5哈希值长度
        assert "test_draft_id" in task_id1  # 应该包含草稿ID信息
    
    @patch('src.service.video_generation_service.time.time')
    def test_create_generation_task(self, mock_time):
        """测试创建生成任务"""
        mock_time.return_value = 1234567890
        
        task_data = {
            "draft_id": "test_draft_id",
            "output_format": "mp4",
            "quality": "high",
            "resolution": "1080p"
        }
        
        task = self.video_service.create_generation_task(task_data)
        
        assert task["draft_id"] == "test_draft_id"
        assert task["output_format"] == "mp4"
        assert task["quality"] == "high"
        assert task["status"] == "pending"
        assert task["created_at"] == 1234567890
        assert "task_id" in task
    
    def test_update_task_status(self):
        """测试更新任务状态"""
        task = {
            "task_id": "test_task_id",
            "status": "pending",
            "progress": 0
        }
        
        # 更新为处理中状态
        self.video_service.update_task_status(task, "processing", 50)
        assert task["status"] == "processing"
        assert task["progress"] == 50
        
        # 更新为完成状态
        self.video_service.update_task_status(task, "completed", 100)
        assert task["status"] == "completed"
        assert task["progress"] == 100
    
    def test_calculate_generation_progress(self):
        """测试计算生成进度"""
        # 模拟不同阶段的进度计算
        progress = self.video_service.calculate_generation_progress("preparing")
        assert 0 <= progress <= 10
        
        progress = self.video_service.calculate_generation_progress("processing")
        assert 10 <= progress <= 90
        
        progress = self.video_service.calculate_generation_progress("finalizing")
        assert 90 <= progress <= 100
        
        progress = self.video_service.calculate_generation_progress("completed")
        assert progress == 100
    
    @patch('src.service.video_generation_service.time.time')
    def test_check_task_timeout(self, mock_time):
        """测试检查任务超时"""
        mock_time.return_value = 1000
        
        # 测试未超时任务
        task = {"created_at": 900}  # 100秒前创建
        result = self.video_service.check_task_timeout(task, timeout=200)
        assert result is False
        
        # 测试超时任务
        task = {"created_at": 700}  # 300秒前创建
        result = self.video_service.check_task_timeout(task, timeout=200)
        assert result is True
    
    def test_validate_output_settings(self):
        """测试验证输出设置"""
        valid_settings = {
            "format": "mp4",
            "codec": "h264",
            "bitrate": "5000k",
            "resolution": "1920x1080",
            "fps": 30
        }
        
        result = self.video_service.validate_output_settings(valid_settings)
        assert result is True
    
    def test_estimate_generation_time(self):
        """测试估算生成时间"""
        # 基于草稿复杂度估算生成时间
        simple_draft = {"duration": 60, "materials": 2, "effects": 1}
        estimated_time = self.video_service.estimate_generation_time(simple_draft)
        assert estimated_time > 0
        
        complex_draft = {"duration": 300, "materials": 10, "effects": 5}
        complex_time = self.video_service.estimate_generation_time(complex_draft)
        assert complex_time > estimated_time  # 复杂草稿应该需要更长时间

37.5 异常场景测试

37.5.1 网络异常测试

网络异常是常见的错误场景,需要充分测试:

# tests/test_network_exceptions.py
import pytest
import requests
from unittest.mock import Mock, patch
from fastapi.testclient import TestClient
from src.exceptions import CustomException, CustomError

class TestNetworkExceptions:
    """网络异常测试类"""
    
    @patch('requests.get')
    def test_network_timeout_exception(self, mock_get):
        """测试网络超时异常"""
        mock_get.side_effect = requests.exceptions.Timeout("Connection timed out")
        
        with pytest.raises(CustomException) as exc_info:
            # 模拟网络请求超时
            requests.get("http://example.com/api", timeout=5)
        
        # 这里应该包装为自定义异常
        assert "timeout" in str(exc_info.value).lower()
    
    @patch('requests.get')
    def test_network_connection_error(self, mock_get):
        """测试网络连接错误"""
        mock_get.side_effect = requests.exceptions.ConnectionError("Connection refused")
        
        with pytest.raises(CustomException) as exc_info:
            requests.get("http://example.com/api")
        
        assert "connection" in str(exc_info.value).lower()
    
    @patch('requests.get')
    def test_network_http_error(self, mock_get):
        """测试HTTP错误"""
        mock_response = Mock()
        mock_response.status_code = 503
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("503 Service Unavailable")
        mock_get.return_value = mock_response
        
        response = requests.get("http://example.com/api")
        
        with pytest.raises(requests.exceptions.HTTPError):
            response.raise_for_status()
    
    def test_external_service_unavailable(self, test_client: TestClient):
        """测试外部服务不可用"""
        # 模拟外部服务调用失败
        with patch('src.service.external_service.requests.get') as mock_get:
            mock_get.side_effect = requests.exceptions.RequestException("Service unavailable")
            
            response = test_client.post("/v1/draft/create", json={
                "draft_name": "test_draft",
                "resolution": "1080p"
            })
            
            assert response.status_code == 503
            data = response.json()
            assert data["success"] is False
            assert data["error"]["code"] == 9002  # 服务暂不可用

37.5.2 文件系统异常测试

文件系统异常需要特殊处理:

# tests/test_filesystem_exceptions.py
import pytest
import os
from unittest.mock import Mock, patch
from src.exceptions import CustomException, CustomError

class TestFilesystemExceptions:
    """文件系统异常测试类"""
    
    @patch('builtins.open')
    def test_file_not_found_exception(self, mock_open):
        """测试文件未找到异常"""
        mock_open.side_effect = FileNotFoundError("File not found")
        
        with pytest.raises(CustomException) as exc_info:
            with open("/nonexistent/file.txt", 'r') as f:
                content = f.read()
        
        assert exc_info.value.error_code == CustomError.FILE_NOT_FOUND
    
    @patch('builtins.open')
    def test_permission_denied_exception(self, mock_open):
        """测试权限拒绝异常"""
        mock_open.side_effect = PermissionError("Permission denied")
        
        with pytest.raises(CustomException) as exc_info:
            with open("/protected/file.txt", 'w') as f:
                f.write("test content")
        
        assert exc_info.value.error_code == CustomError.PERMISSION_DENIED
    
    @patch('os.makedirs')
    def test_disk_space_exception(self, mock_makedirs):
        """测试磁盘空间不足异常"""
        mock_makedirs.side_effect = OSError("No space left on device")
        
        with pytest.raises(CustomException) as exc_info:
            os.makedirs("/test/directory", exist_ok=True)
        
        assert exc_info.value.error_code == CustomError.DISK_SPACE_EXCEEDED
    
    @patch('os.path.exists')
    def test_corrupted_file_exception(self, mock_exists):
        """测试文件损坏异常"""
        mock_exists.return_value = True
        
        # 模拟读取损坏的文件
        with patch('builtins.open') as mock_open:
            mock_file = Mock()
            mock_file.read.side_effect = UnicodeDecodeError(
                'utf-8', b'', 0, 1, 'invalid start byte'
            )
            mock_open.return_value.__enter__.return_value = mock_file
            
            with pytest.raises(CustomException) as exc_info:
                with open("/corrupted/file.txt", 'r', encoding='utf-8') as f:
                    content = f.read()
            
            assert exc_info.value.error_code == CustomError.CORRUPTED_FILE

37.5.3 数据库异常测试

数据库异常需要特殊处理:

# tests/test_database_exceptions.py
import pytest
from unittest.mock import Mock, patch
from src.exceptions import CustomException, CustomError

class TestDatabaseExceptions:
    """数据库异常测试类"""
    
    def test_database_connection_exception(self):
        """测试数据库连接异常"""
        # 模拟数据库连接失败
        with patch('src.database.connection.pymysql.connect') as mock_connect:
            mock_connect.side_effect = Exception("Connection refused")
            
            with pytest.raises(CustomException) as exc_info:
                # 这里应该包装为自定义异常
                mock_connect()
            
            assert "database connection" in str(exc_info.value).lower()
    
    def test_database_timeout_exception(self):
        """测试数据库查询超时异常"""
        # 模拟数据库查询超时
        with patch('src.database.operations.execute_query') as mock_query:
            mock_query.side_effect = TimeoutError("Query timeout")
            
            with pytest.raises(CustomException) as exc_info:
                mock_query("SELECT * FROM drafts")
            
            assert exc_info.value.error_code == CustomError.DATABASE_TIMEOUT
    
    def test_database_constraint_violation(self):
        """测试数据库约束违反异常"""
        # 模拟唯一约束违反
        with patch('src.database.operations.execute_query') as mock_query:
            mock_query.side_effect = Exception("Duplicate entry")
            
            with pytest.raises(CustomException) as exc_info:
                mock_query("INSERT INTO drafts (name) VALUES ('duplicate')")
            
            assert exc_info.value.error_code == CustomError.DATABASE_CONSTRAINT_VIOLATION
    
    def test_database_transaction_rollback(self):
        """测试数据库事务回滚"""
        # 模拟事务回滚场景
        with patch('src.database.operations.begin_transaction') as mock_begin:
            with patch('src.database.operations.rollback_transaction') as mock_rollback:
                mock_begin.side_effect = Exception("Transaction failed")
                
                try:
                    mock_begin()
                except Exception:
                    mock_rollback()
                
                mock_rollback.assert_called_once()

37.5.4 并发异常测试

并发场景下可能出现各种异常,需要充分测试:

# tests/test_concurrency_exceptions.py
import pytest
import threading
import time
from unittest.mock import Mock, patch
from src.exceptions import CustomException, CustomError

class TestConcurrencyExceptions:
    """并发异常测试类"""
    
    def test_race_condition_in_draft_creation(self):
        """测试草稿创建中的竞态条件"""
        # 模拟并发创建相同名称的草稿
        results = []
        
        def create_draft(draft_name):
            try:
                # 模拟草稿创建逻辑
                with patch('src.service.draft_service.check_draft_exists') as mock_check:
                    mock_check.return_value = False  # 初始检查时不存在
                    
                    # 模拟并发创建
                    time.sleep(0.1)  # 模拟处理延迟
                    
                    # 模拟第二个线程已经创建了草稿
                    if len(results) > 0:
                        mock_check.return_value = True
                        raise CustomException(
                            error_code=CustomError.DRAFT_ALREADY_EXISTS,
                            message="Draft already exists"
                        )
                    
                    results.append(f"Draft {draft_name} created")
                    
            except CustomException as e:
                results.append(f"Error: {e.message}")
        
        # 启动两个线程并发创建相同草稿
        thread1 = threading.Thread(target=create_draft, args=("test_draft",))
        thread2 = threading.Thread(target=create_draft, args=("test_draft",))
        
        thread1.start()
        thread2.start()
        thread1.join()
        thread2.join()
        
        # 验证只有一个线程成功创建草稿
        success_count = sum(1 for result in results if "created" in result)
        error_count = sum(1 for result in results if "Error" in result)
        
        assert success_count == 1
        assert error_count == 1
    
    def test_deadlock_prevention(self):
        """测试死锁预防机制"""
        # 模拟资源锁定场景
        resource_lock = threading.Lock()
        
        def access_resource_with_timeout(resource_id, timeout=5):
            """带超时的资源访问"""
            start_time = time.time()
            
            while time.time() - start_time < timeout:
                if resource_lock.acquire(timeout=1):
                    try:
                        # 模拟资源访问
                        time.sleep(0.1)
                        return f"Resource {resource_id} accessed successfully"
                    finally:
                        resource_lock.release()
                
                time.sleep(0.1)
            
            raise CustomException(
                error_code=CustomError.RESOURCE_LOCK_TIMEOUT,
                message=f"Timeout accessing resource {resource_id}"
            )
        
        # 测试正常访问
        result = access_resource_with_timeout("test_resource")
        assert "accessed successfully" in result
    
    def test_resource_exhaustion_handling(self):
        """测试资源耗尽处理"""
        # 模拟内存耗尽
        with patch('src.service.video_generation_service.psutil.virtual_memory') as mock_memory:
            mock_memory.return_value.available = 100 * 1024 * 1024  # 100MB可用
            mock_memory.return_value.total = 8 * 1024 * 1024 * 1024  # 8GB总内存
            
            # 模拟需要大量内存的操作
            with pytest.raises(CustomException) as exc_info:
                # 这里应该检查内存是否足够
                if mock_memory.return_value.available < 500 * 1024 * 1024:  # 需要500MB
                    raise CustomException(
                        error_code=CustomError.INSUFFICIENT_MEMORY,
                        message="Insufficient memory for video generation"
                    )
            
            assert exc_info.value.error_code == CustomError.INSUFFICIENT_MEMORY

37.6 性能测试

37.6.1 API响应时间测试

性能测试确保API响应时间在可接受范围内:

# tests/test_performance.py
import pytest
import time
import statistics
from concurrent.futures import ThreadPoolExecutor, as_completed
from fastapi.testclient import TestClient
from tests.utils.test_helpers import TestHelpers
from tests.config import test_config

class TestPerformance:
    """性能测试类"""
    
    def measure_response_time(self, func, *args, **kwargs):
        """测量函数响应时间"""
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        
        response_time = end_time - start_time
        return result, response_time
    
    def test_api_response_time(self, test_client: TestClient):
        """测试API响应时间"""
        response_times = []
        
        # 测试健康检查接口
        for _ in range(10):
            _, response_time = self.measure_response_time(
                test_client.get, "/health"
            )
            response_times.append(response_time)
        
        # 计算统计信息
        avg_response_time = statistics.mean(response_times)
        max_response_time = max(response_times)
        min_response_time = min(response_times)
        
        print(f"健康检查接口响应时间统计:")
        print(f"平均响应时间: {avg_response_time:.3f}s")
        print(f"最大响应时间: {max_response_time:.3f}s")
        print(f"最小响应时间: {min_response_time:.3f}s")
        
        # 断言响应时间应该在合理范围内
        assert avg_response_time < 0.1  # 平均响应时间应小于100ms
        assert max_response_time < 0.5   # 最大响应时间应小于500ms
    
    def test_concurrent_api_requests(self, test_client: TestClient):
        """测试并发API请求性能"""
        def make_request():
            response = test_client.get("/health")
            return response.status_code
        
        # 并发执行请求
        start_time = time.time()
        
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [executor.submit(make_request) for _ in range(50)]
            
            results = []
            for future in as_completed(futures):
                try:
                    result = future.result()
                    results.append(result)
                except Exception as e:
                    results.append(f"Error: {str(e)}")
        
        end_time = time.time()
        total_time = end_time - start_time
        
        # 验证结果
        success_count = sum(1 for result in results if result == 200)
        error_count = len(results) - success_count
        
        print(f"并发请求测试结果:")
        print(f"总请求数: {len(results)}")
        print(f"成功请求数: {success_count}")
        print(f"失败请求数: {error_count}")
        print(f"总耗时: {total_time:.3f}s")
        print(f"平均响应时间: {total_time/len(results):.3f}s")
        
        # 断言成功率
        success_rate = success_count / len(results)
        assert success_rate > 0.95  # 成功率应大于95%
    
    def test_database_query_performance(self):
        """测试数据库查询性能"""
        # 模拟数据库查询
        def simulate_db_query():
            time.sleep(0.01)  # 模拟10ms的数据库查询时间
            return {"result": "data"}
        
        query_times = []
        
        for _ in range(20):
            _, query_time = self.measure_response_time(simulate_db_query)
            query_times.append(query_time)
        
        avg_query_time = statistics.mean(query_times)
        
        print(f"数据库查询性能:")
        print(f"平均查询时间: {avg_query_time:.3f}s")
        
        assert avg_query_time < 0.05  # 平均查询时间应小于50ms
    
    def test_file_upload_performance(self, test_client: TestClient):
        """测试文件上传性能"""
        # 创建测试文件数据
        file_size = 1024 * 1024  # 1MB
        test_data = b"0" * file_size
        
        import io
        test_file = io.BytesIO(test_data)
        
        # 测试上传时间
        _, upload_time = self.measure_response_time(
            test_client.post,
            "/v1/upload",
            files={"file": ("test.txt", test_file, "text/plain")}
        )
        
        upload_speed = file_size / upload_time / 1024 / 1024  # MB/s
        
        print(f"文件上传性能:")
        print(f"文件大小: {file_size / 1024 / 1024:.2f}MB")
        print(f"上传时间: {upload_time:.3f}s")
        print(f"上传速度: {upload_speed:.2f}MB/s")
        
        assert upload_speed > 1.0  # 上传速度应大于1MB/s
    
    def test_memory_usage_under_load(self):
        """测试负载下的内存使用情况"""
        import psutil
        import gc
        
        # 获取初始内存使用
        process = psutil.Process()
        initial_memory = process.memory_info().rss / 1024 / 1024  # MB
        
        # 模拟高负载操作
        def memory_intensive_operation():
            # 创建大量对象
            large_list = [i for i in range(100000)]
            large_dict = {str(i): i for i in range(10000)}
            
            # 进行一些计算
            result = sum(large_list) + sum(large_dict.values())
            
            # 清理引用
            del large_list
            del large_dict
            
            # 强制垃圾回收
            gc.collect()
            
            return result
        
        # 执行多次内存密集型操作
        for _ in range(10):
            memory_intensive_operation()
        
        # 获取最终内存使用
        final_memory = process.memory_info().rss / 1024 / 1024  # MB
        memory_increase = final_memory - initial_memory
        
        print(f"内存使用测试:")
        print(f"初始内存: {initial_memory:.2f}MB")
        print(f"最终内存: {final_memory:.2f}MB")
        print(f"内存增长: {memory_increase:.2f}MB")
        
        assert memory_increase < 100  # 内存增长应小于100MB

37.6.2 负载测试

负载测试确保系统在高并发情况下仍能正常工作:

# tests/test_load_testing.py
import pytest
import time
import threading
import queue
from concurrent.futures import ThreadPoolExecutor, as_completed
from fastapi.testclient import TestClient
from tests.config import test_config

class TestLoadTesting:
    """负载测试类"""
    
    def test_api_rate_limiting(self, test_client: TestClient):
        """测试API速率限制"""
        results = []
        
        def make_request():
            try:
                response = test_client.get("/health")
                return {
                    "status_code": response.status_code,
                    "timestamp": time.time()
                }
            except Exception as e:
                return {
                    "status_code": 0,
                    "error": str(e),
                    "timestamp": time.time()
                }
        
        # 快速发送大量请求
        start_time = time.time()
        
        with ThreadPoolExecutor(max_workers=20) as executor:
            # 在1秒内发送100个请求
            futures = []
            for i in range(100):
                futures.append(executor.submit(make_request))
                if i % 10 == 0:  # 每10个请求暂停一下
                    time.sleep(0.1)
            
            for future in as_completed(futures):
                results.append(future.result())
        
        end_time = time.time()
        total_time = end_time - start_time
        
        # 分析结果
        success_count = sum(1 for result in results if result["status_code"] == 200)
        rate_limited_count = sum(1 for result in results if result["status_code"] == 429)
        error_count = len(results) - success_count - rate_limited_count
        
        print(f"速率限制测试:")
        print(f"总请求数: {len(results)}")
        print(f"成功请求: {success_count}")
        print(f"速率限制: {rate_limited_count}")
        print(f"错误请求: {error_count}")
        print(f"总耗时: {total_time:.3f}s")
        
        # 验证速率限制生效
        assert rate_limited_count > 0  # 应该有一些请求被速率限制
        assert success_count > 0       # 应该有一些请求成功
    
    def test_database_connection_pool(self):
        """测试数据库连接池性能"""
        # 模拟数据库连接池
        class MockConnectionPool:
            def __init__(self, max_connections=10):
                self.max_connections = max_connections
                self.connections = queue.Queue(maxsize=max_connections)
                self.active_connections = 0
            
            def get_connection(self):
                if self.active_connections < self.max_connections:
                    self.active_connections += 1
                    return Mock()
                else:
                    # 等待可用连接
                    time.sleep(0.1)
                    return self.get_connection()
            
            def release_connection(self, conn):
                self.active_connections -= 1
        
        pool = MockConnectionPool(max_connections=5)
        results = []
        
        def use_connection():
            conn = pool.get_connection()
            time.sleep(0.01)  # 模拟数据库操作
            pool.release_connection(conn)
            return "success"
        
        # 并发使用连接
        with ThreadPoolExecutor(max_workers=20) as executor:
            futures = [executor.submit(use_connection) for _ in range(50)]
            
            for future in as_completed(futures):
                try:
                    result = future.result()
                    results.append(result)
                except Exception as e:
                    results.append(f"error: {str(e)}")
        
        success_count = sum(1 for result in results if result == "success")
        
        print(f"数据库连接池测试:")
        print(f"总请求数: 50")
        print(f"连接池大小: 5")
        print(f"成功数: {success_count}")
        
        assert success_count == 50  # 所有请求都应该成功
    
    def test_queue_processing_performance(self):
        """测试队列处理性能"""
        # 模拟任务队列
        task_queue = queue.Queue()
        results = []
        
        def producer():
            for i in range(100):
                task_queue.put({"id": i, "data": f"task_{i}"})
                time.sleep(0.001)  # 模拟任务产生
        
        def consumer():
            while True:
                try:
                    task = task_queue.get(timeout=1)
                    # 模拟任务处理
                    time.sleep(0.005)
                    results.append(f"processed_{task['id']}")
                    task_queue.task_done()
                except queue.Empty:
                    break
        
        # 启动生产者和消费者
        producer_thread = threading.Thread(target=producer)
        consumer_threads = [threading.Thread(target=consumer) for _ in range(5)]
        
        start_time = time.time()
        
        producer_thread.start()
        for thread in consumer_threads:
            thread.start()
        
        producer_thread.join()
        task_queue.join()  # 等待所有任务完成
        
        end_time = time.time()
        total_time = end_time - start_time
        
        processing_rate = len(results) / total_time  # 任务/秒
        
        print(f"队列处理性能:")
        print(f"总任务数: 100")
        print(f"消费者数: 5")
        print(f"处理完成数: {len(results)}")
        print(f"总耗时: {total_time:.3f}s")
        print(f"处理速率: {processing_rate:.2f}任务/秒")
        
        assert len(results) == 100  # 所有任务都应该被处理
        assert processing_rate > 50  # 处理速率应大于50任务/秒

37.6.3 压力测试

压力测试确保系统在极限情况下不会崩溃:

# tests/test_stress_testing.py
import pytest
import time
import threading
import psutil
import gc
from concurrent.futures import ThreadPoolExecutor, as_completed
from fastapi.testclient import TestClient

class TestStressTesting:
    """压力测试类"""
    
    def test_memory_leak_detection(self):
        """测试内存泄漏检测"""
        import objgraph
        
        # 获取初始对象数量
        initial_objects = {}
        gc.collect()
        
        for obj_type in ['list', 'dict', 'str']:
            initial_objects[obj_type] = objgraph.count(obj_type)
        
        # 模拟可能导致内存泄漏的操作
        def leaky_operation():
            # 创建循环引用
            class Node:
                def __init__(self):
                    self.children = []
                    self.parent = None
            
            nodes = []
            for i in range(1000):
                node = Node()
                nodes.append(node)
                if i > 0:
                    node.parent = nodes[i-1]
                    nodes[i-1].children.append(node)
            
            # 清理引用,但循环引用仍然存在
            del nodes
        
        # 执行多次操作
        for _ in range(10):
            leaky_operation()
            gc.collect()  # 尝试垃圾回收
        
        # 检查对象数量增长
        final_objects = {}
        for obj_type in ['list', 'dict', 'str']:
            final_objects[obj_type] = objgraph.count(obj_type)
        
        # 分析结果
        leak_detected = False
        for obj_type in initial_objects:
            growth = final_objects[obj_type] - initial_objects[obj_type]
            growth_rate = growth / initial_objects[obj_type] * 100 if initial_objects[obj_type] > 0 else 0
            
            print(f"{obj_type}对象数量变化: {initial_objects[obj_type]} -> {final_objects[obj_type]} (增长: {growth}, {growth_rate:.1f}%)")
            
            if growth_rate > 10:  # 超过10%的增长视为潜在泄漏
                leak_detected = True
        
        assert not leak_detected, "检测到潜在的内存泄漏"
    
    def test_cpu_usage_under_stress(self):
        """测试压力下的CPU使用率"""
        import psutil
        
        def cpu_intensive_task():
            """CPU密集型任务"""
            result = 0
            for i in range(1000000):
                result += i ** 2
            return result
        
        # 获取初始CPU使用率
        initial_cpu = psutil.cpu_percent(interval=1)
        
        # 启动多个CPU密集型任务
        with ThreadPoolExecutor(max_workers=psutil.cpu_count()) as executor:
            futures = [executor.submit(cpu_intensive_task) for _ in range(100)]
            
            # 监控CPU使用率
            cpu_readings = []
            for _ in range(10):
                cpu_readings.append(psutil.cpu_percent(interval=0.1))
            
            # 等待所有任务完成
            for future in as_completed(futures):
                future.result()
        
        max_cpu = max(cpu_readings)
        avg_cpu = sum(cpu_readings) / len(cpu_readings)
        
        print(f"CPU压力测试:")
        print(f"初始CPU使用率: {initial_cpu:.1f}%")
        print(f"平均CPU使用率: {avg_cpu:.1f}%")
        print(f"峰值CPU使用率: {max_cpu:.1f}%")
        
        assert max_cpu > 50  # 峰值CPU使用率应超过50%
        assert avg_cpu > 20  # 平均CPU使用率应超过20%
    
    def test_disk_io_performance(self):
        """测试磁盘I/O性能"""
        import tempfile
        import os
        
        # 创建临时文件进行测试
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_path = temp_file.name
        
        try:
            # 写入性能测试
            test_data = b"0" * (10 * 1024 * 1024)  # 10MB数据
            
            start_time = time.time()
            with open(temp_path, 'wb') as f:
                f.write(test_data)
            write_time = time.time() - start_time
            
            # 读取性能测试
            start_time = time.time()
            with open(temp_path, 'rb') as f:
                read_data = f.read()
            read_time = time.time() - start_time
            
            # 计算性能指标
            write_speed = len(test_data) / write_time / 1024 / 1024  # MB/s
            read_speed = len(read_data) / read_time / 1024 / 1024   # MB/s
            
            print(f"磁盘I/O性能测试:")
            print(f"写入速度: {write_speed:.2f}MB/s")
            print(f"读取速度: {read_speed:.2f}MB/s")
            print(f"写入时间: {write_time:.3f}s")
            print(f"读取时间: {read_time:.3f}s")
            
            assert write_speed > 10  # 写入速度应大于10MB/s
            assert read_speed > 50   # 读取速度应大于50MB/s
            
        finally:
            # 清理临时文件
            if os.path.exists(temp_path):
                os.unlink(temp_path)
    
    def test_api_stability_under_stress(self, test_client: TestClient):
        """测试压力下API稳定性"""
        def stress_test_request():
            """压力测试请求"""
            try:
                # 随机选择API端点
                import random
                endpoints = ["/health", "/api/version", "/v1/draft/create"]
                endpoint = random.choice(endpoints)
                
                if endpoint == "/v1/draft/create":
                    response = test_client.post(endpoint, json={
                        "draft_name": f"stress_test_{random.randint(1, 10000)}",
                        "resolution": "1080p"
                    })
                else:
                    response = test_client.get(endpoint)
                
                return {
                    "endpoint": endpoint,
                    "status_code": response.status_code,
                    "success": response.status_code == 200
                }
            except Exception as e:
                return {
                    "endpoint": endpoint,
                    "error": str(e),
                    "success": False
                }
        
        # 持续进行压力测试
        stress_duration = 30  # 30秒
        start_time = time.time()
        
        results = []
        with ThreadPoolExecutor(max_workers=50) as executor:
            while time.time() - start_time < stress_duration:
                futures = [executor.submit(stress_test_request) for _ in range(100)]
                
                for future in as_completed(futures):
                    try:
                        result = future.result()
                        results.append(result)
                    except Exception as e:
                        results.append({"success": False, "error": str(e)})
                
                time.sleep(0.1)  # 短暂暂停
        
        # 分析结果
        total_requests = len(results)
        success_requests = sum(1 for result in results if result.get("success"))
        error_requests = total_requests - success_requests
        
        success_rate = success_requests / total_requests * 100
        
        print(f"API压力测试 ({stress_duration}秒):")
        print(f"总请求数: {total_requests}")
        print(f"成功请求: {success_requests}")
        print(f"失败请求: {error_requests}")
        print(f"成功率: {success_rate:.1f}%")
        
        assert success_rate > 90  # 成功率应大于90%
        assert error_requests < total_requests * 0.1  # 失败请求应小于10%

## 37.7 测试自动化与持续集成

### 37.7.1 测试自动化框架

测试自动化框架确保测试能够持续运行并及时发现问题:

```python
# tests/automation/test_runner.py
import os
import sys
import time
import logging
import subprocess
from datetime import datetime
from typing import List, Dict, Any
from pathlib import Path

class TestAutomationRunner:
    """测试自动化运行器"""
    
    def __init__(self, config_path: str = None):
        self.config_path = config_path or "tests/config/test_automation.json"
        self.logger = self._setup_logging()
        self.results = []
    
    def _setup_logging(self):
        """设置日志记录"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('logs/test_automation.log'),
                logging.StreamHandler()
            ]
        )
        return logging.getLogger(__name__)
    
    def run_test_suite(self, test_pattern: str = "tests/test_*.py") -> Dict[str, Any]:
        """运行测试套件"""
        self.logger.info(f"开始运行测试套件: {test_pattern}")
        
        start_time = time.time()
        
        # 构建pytest命令
        cmd = [
            "python", "-m", "pytest",
            test_pattern,
            "-v",  # 详细输出
            "--tb=short",  # 简短错误回溯
            "--html=reports/test_report.html",  # HTML报告
            "--json-report",  # JSON报告
            "--json-report-file=reports/test_report.json"
        ]
        
        try:
            # 运行测试
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=3600  # 1小时超时
            )
            
            execution_time = time.time() - start_time
            
            test_results = {
                "test_pattern": test_pattern,
                "exit_code": result.returncode,
                "stdout": result.stdout,
                "stderr": result.stderr,
                "execution_time": execution_time,
                "timestamp": datetime.now().isoformat()
            }
            
            self.logger.info(f"测试执行完成,耗时: {execution_time:.2f}秒")
            return test_results
            
        except subprocess.TimeoutExpired:
            self.logger.error("测试执行超时")
            return {
                "test_pattern": test_pattern,
                "exit_code": -1,
                "error": "Test execution timeout",
                "execution_time": time.time() - start_time
            }
        except Exception as e:
            self.logger.error(f"测试执行失败: {str(e)}")
            return {
                "test_pattern": test_pattern,
                "exit_code": -1,
                "error": str(e),
                "execution_time": time.time() - start_time
            }
    
    def run_performance_tests(self) -> Dict[str, Any]:
        """运行性能测试"""
        self.logger.info("开始运行性能测试")
        
        performance_tests = [
            "tests/test_performance.py::TestPerformance::test_api_response_time",
            "tests/test_performance.py::TestPerformance::test_concurrent_api_requests",
            "tests/test_load_testing.py::TestLoadTesting::test_api_rate_limiting",
            "tests/test_stress_testing.py::TestStressTesting::test_memory_leak_detection"
        ]
        
        results = []
        for test in performance_tests:
            self.logger.info(f"运行性能测试: {test}")
            result = self.run_test_suite(test)
            results.append(result)
        
        return {
            "test_type": "performance",
            "total_tests": len(performance_tests),
            "results": results,
            "timestamp": datetime.now().isoformat()
        }
    
    def generate_test_report(self, results: List[Dict]) -> str:
        """生成测试报告"""
        report_path = f"reports/test_automation_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>测试自动化报告</title>
            <style>
                body {{ font-family: Arial, sans-serif; margin: 20px; }}
                .header {{ background-color: #f4f4f4; padding: 20px; border-radius: 5px; }}
                .test-result {{ margin: 10px 0; padding: 10px; border-radius: 3px; }}
                .success {{ background-color: #d4edda; border: 1px solid #c3e6cb; }}
                .failure {{ background-color: #f8d7da; border: 1px solid #f5c6cb; }}
                .stats {{ background-color: #d1ecf1; border: 1px solid #bee5eb; padding: 15px; margin: 20px 0; }}
                table {{ border-collapse: collapse; width: 100%; }}
                th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
                th {{ background-color: #f2f2f2; }}
            </style>
        </head>
        <body>
            <div class="header">
                <h1>测试自动化报告</h1>
                <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
            </div>
            
            <div class="stats">
                <h3>测试统计</h3>
                <p>总测试数: {len(results)}</p>
                <p>成功数: {sum(1 for r in results if r.get('exit_code') == 0)}</p>
                <p>失败数: {sum(1 for r in results if r.get('exit_code') != 0)}</p>
            </div>
            
            <h3>详细结果</h3>
            <table>
                <tr>
                    <th>测试套件</th>
                    <th>状态</th>
                    <th>执行时间</th>
                    <th>错误信息</th>
                </tr>
                {''.join(self._generate_table_rows(results))}
            </table>
        </body>
        </html>
        """
        
        # 确保报告目录存在
        os.makedirs(os.path.dirname(report_path), exist_ok=True)
        
        with open(report_path, 'w', encoding='utf-8') as f:
            f.write(html_content)
        
        self.logger.info(f"测试报告已生成: {report_path}")
        return report_path
    
    def _generate_table_rows(self, results: List[Dict]) -> str:
        """生成表格行"""
        rows = []
        for result in results:
            status = "✅ 成功" if result.get('exit_code') == 0 else "❌ 失败"
            execution_time = f"{result.get('execution_time', 0):.2f}s"
            error_info = result.get('error', '') or result.get('stderr', '')[:100]
            
            row = f"""
            <tr>
                <td>{result.get('test_pattern', 'Unknown')}</td>
                <td>{status}</td>
                <td>{execution_time}</td>
                <td>{error_info}</td>
            </tr>
            """
            rows.append(row)
        
        return ''.join(rows)
    
    def run_continuous_testing(self, interval_hours: int = 24):
        """运行持续测试"""
        self.logger.info(f"开始持续测试模式,间隔: {interval_hours}小时")
        
        while True:
            try:
                # 运行完整测试套件
                results = []
                
                # 单元测试
                unit_results = self.run_test_suite("tests/test_*.py")
                results.append(unit_results)
                
                # 性能测试
                perf_results = self.run_performance_tests()
                results.extend(perf_results.get('results', []))
                
                # 生成报告
                report_path = self.generate_test_report(results)
                
                self.logger.info(f"测试完成,报告: {report_path}")
                
                # 等待下一次运行
                time.sleep(interval_hours * 3600)
                
            except KeyboardInterrupt:
                self.logger.info("持续测试被中断")
                break
            except Exception as e:
                self.logger.error(f"持续测试出错: {str(e)}")
                time.sleep(3600)  # 出错后等待1小时再试

if __name__ == "__main__":
    runner = TestAutomationRunner()
    
    # 运行单次测试
    results = runner.run_test_suite()
    report_path = runner.generate_test_report([results])
    
    print(f"测试完成,报告: {report_path}")

## 37.8 测试最佳实践总结

### 37.8.1 测试设计原则

1. **独立性原则**:每个测试用例应该独立运行,不依赖其他测试的结果
2. **可重复性原则**:测试用例应该可以在任何环境下重复运行,结果一致
3. **简单性原则**:测试用例应该简单明了,易于理解和维护
4. **完整性原则**:测试用例应该覆盖所有重要的业务逻辑和边界条件
5. **性能原则**:测试用例应该快速执行,不影响开发效率

### 37.8.2 测试代码规范

1. **命名规范**:测试函数名应该清晰描述测试的目的和场景
2. **注释规范**:复杂的测试逻辑应该有详细的注释说明
3. **结构规范**:测试代码应该结构清晰,遵循AAA模式(Arrange-Act-Assert)
4. **异常处理**:测试代码应该正确处理异常,避免测试失败影响其他测试
5. **资源管理**:测试代码应该正确管理资源,避免资源泄漏

### 37.8.3 测试数据管理

1. **测试数据分离**:测试数据应该与测试代码分离,便于维护
2. **数据清理**:测试完成后应该清理测试数据,避免影响其他测试
3. **数据复用**:合理使用测试数据,避免重复创建相同的数据
4. **数据验证**:测试数据应该经过验证,确保数据的有效性和一致性

### 37.8.4 测试环境管理

1. **环境隔离**:测试环境应该与开发环境和生产环境隔离
2. **环境一致性**:测试环境应该尽可能与生产环境保持一致
3. **环境可重复**:测试环境应该可以快速重建,确保测试的可重复性
4. **环境监控**:测试环境应该进行监控,及时发现环境问题

### 37.8.5 测试持续改进

1. **测试覆盖率**:定期检查和提升测试覆盖率
2. **测试效率**:持续优化测试用例,提升测试执行效率
3. **测试质量**:定期评审测试用例,确保测试质量
4. **测试自动化**:持续推进测试自动化,减少人工干预
5. **测试反馈**:建立测试反馈机制,及时发现和修复问题

通过遵循这些最佳实践,可以构建一个高质量、高效率的测试体系,为剪映小助手的稳定性和可靠性提供有力保障。

---

## 附录

**代码仓库地址:**
- GitHub: `https://github.com/Hommy-master/capcut-mate`
- Gitee: `https://gitee.com/taohongmin-gitee/capcut-mate`

**接口文档地址:**
- API文档地址: `https://docs.jcaigc.cn`