第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`