
分层架构设计与核心组件
大约 11 分钟
分层架构设计与核心组件
前言:架构设计的"道"与"术"
还记得我刚开始写测试代码时,就像一个刚学会炒菜的新手,把所有食材都往锅里倒,虽然最后也能吃,但是味道嘛...一言难尽。后来随着项目越来越复杂,我才明白好的架构就像一道精心设计的菜谱,每个组件都有自己的职责,相互配合才能做出美味佳肴。
今天我们就来深入探讨这个pytest框架的分层架构设计,看看它是如何做到既简单易用又功能强大的。这不是纸上谈兵的理论课,而是基于真实项目经验的实战分享。
架构设计理念:SOLID原则的实践
为什么需要分层架构?
想象一下,如果把所有代码都写在一个文件里会怎样?就像把客厅、卧室、厨房、卫生间都放在一个房间里,虽然功能都有,但是...你懂的。
分层架构的好处:
- 职责清晰:每一层都有明确的职责,不会出现"什么都管,什么都管不好"的情况
- 易于维护:修改某一层的代码不会影响其他层
- 便于测试:可以单独测试每一层的功能
- 可扩展性:新增功能时只需要在对应层添加代码
五层架构设计
"""
pytest框架的五层架构设计
就像一栋精心设计的办公楼,每一层都有自己的功能
"""
architecture_layers = {
"用户层 (User Layer)": {
"职责": "测试用例编写和配置管理",
"组件": ["测试用例", "配置文件", "测试数据"],
"比喻": "办公楼的顶层,用户直接接触的地方"
},
"接口层 (Interface Layer)": {
"职责": "提供统一的操作接口",
"组件": ["CLI命令行", "Web API", "IDE插件"],
"比喻": "前台接待,负责接待和引导"
},
"业务层 (Business Layer)": {
"职责": "业务逻辑处理和流程控制",
"组件": ["测试执行器", "报告生成器", "数据管理器"],
"比喻": "业务部门,负责具体业务处理"
},
"核心层 (Core Layer)": {
"职责": "核心功能实现",
"组件": ["HTTP客户端", "断言引擎", "Mock服务器", "性能测试器"],
"比喻": "技术部门,负责核心技术实现"
},
"基础设施层 (Infrastructure Layer)": {
"职责": "基础服务支撑",
"组件": ["日志系统", "配置管理", "数据库", "文件系统"],
"比喻": "后勤部门,负责基础设施保障"
}
}配置管理模块:项目的"大脑"
1. 配置管理器的设计
# conf/config.py
"""
配置管理器 - 项目的智能大脑
就像家里的智能中控系统,统一管理所有设备的配置
"""
import os
import yaml
from pathlib import Path
from typing import Any, Dict, Optional
class ConfigManager:
"""配置管理器 - 单例模式实现"""
_instance = None
_config_data = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._config_data is None:
self.env = os.getenv('TEST_ENV', 'boe')
self._config_data = self._load_config()
self._validate_config()
def _load_config(self) -> Dict[str, Any]:
"""加载配置文件"""
config_file = Path(__file__).parent / "settings.yaml"
if not config_file.exists():
raise FileNotFoundError(f"配置文件不存在: {config_file}")
try:
with open(config_file, 'r', encoding='utf-8') as f:
all_config = yaml.safe_load(f)
env_config = all_config.get(self.env)
if not env_config:
raise ValueError(f"环境配置不存在: {self.env}")
return env_config
except yaml.YAMLError as e:
raise ValueError(f"配置文件格式错误: {e}")
def _validate_config(self):
"""验证配置完整性"""
required_keys = ['API.base_url', 'API.timeout']
for key in required_keys:
if self.get(key) is None:
raise ValueError(f"必需的配置项缺失: {key}")
def get(self, key: str, default: Any = None) -> Any:
"""
获取配置值,支持点号分隔的嵌套键
Args:
key: 配置键,支持嵌套 (如: 'API.base_url')
default: 默认值
Returns:
配置值
Examples:
>>> config.get('API.base_url')
'https://httpbin.org'
>>> config.get('API.timeout', 30)
30
"""
keys = key.split('.')
value = self._config_data
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def get_env(self) -> str:
"""获取当前环境"""
return self.env
def reload(self):
"""重新加载配置"""
self._config_data = self._load_config()
self._validate_config()
# 全局配置实例
config = ConfigManager()2. 常量定义模块
# conf/constants.py
"""
常量定义 - 项目的"字典"
把所有魔法数字和字符串都放在这里,避免硬编码
"""
class HTTPStatus:
"""HTTP状态码常量"""
OK = 200
CREATED = 201
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500
class Headers:
"""HTTP请求头常量"""
CONTENT_TYPE = "Content-Type"
AUTHORIZATION = "Authorization"
USER_AGENT = "User-Agent"
ACCEPT = "Accept"
class ContentType:
"""内容类型常量"""
JSON = "application/json"
FORM = "application/x-www-form-urlencoded"
XML = "application/xml"
TEXT = "text/plain"
class TestData:
"""测试数据常量"""
DEFAULT_TIMEOUT = 30
MAX_RETRY_TIMES = 3
DEFAULT_PAGE_SIZE = 10
class LogLevel:
"""日志级别常量"""
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
# 环境相关常量
ENVIRONMENTS = {
'boe': '开发环境',
'test': '测试环境',
'staging': '预发布环境',
'prod': '生产环境'
}
# 测试标记常量
TEST_MARKERS = {
'smoke': '冒烟测试',
'regression': '回归测试',
'integration': '集成测试',
'performance': '性能测试'
}核心组件模块:功能实现的"心脏"
1. HTTP客户端模块结构
# src/client/ 目录结构
"""
HTTP客户端模块 - 网络请求的专家
就像一个专业的快递员,负责准确、快速地传递信息
"""
client_module_structure = {
"base_client.py": {
"职责": "基础HTTP客户端实现",
"功能": ["请求发送", "响应处理", "会话管理", "错误处理"],
"设计模式": "模板方法模式"
},
"base_auth.py": {
"职责": "认证处理",
"功能": ["Bearer Token", "Basic Auth", "自定义认证"],
"设计模式": "策略模式"
},
"validatable.py": {
"职责": "响应验证",
"功能": ["状态码验证", "响应时间验证", "内容验证"],
"设计模式": "装饰器模式"
}
}2. 基础客户端实现
# src/client/base_client.py
"""
基础HTTP客户端 - 网络请求的瑞士军刀
"""
import requests
import time
from typing import Dict, Any, Optional, Union
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from conf.config import config
from conf.constants import HTTPStatus, Headers
from src.utils.log_moudle import logger
class BaseClient:
"""
基础HTTP客户端
这个类就像一个专业的邮递员,负责准确、快速地传递HTTP请求
"""
def __init__(self, base_url: str, timeout: Optional[int] = None):
self.base_url = base_url.rstrip('/')
self.timeout = timeout or config.get('API.timeout', 30)
self.session = requests.Session()
# 设置默认请求头
self._setup_default_headers()
# 配置重试策略
self._setup_retry_strategy()
logger.info(f"HTTP客户端初始化完成: {self.base_url}")
def _setup_default_headers(self):
"""设置默认请求头"""
default_headers = {
Headers.USER_AGENT: "pytest-framework/1.0.0",
Headers.ACCEPT: "application/json",
Headers.CONTENT_TYPE: "application/json"
}
self.session.headers.update(default_headers)
def _setup_retry_strategy(self):
"""配置重试策略"""
retry_times = config.get('API.retry_times', 3)
retry_delay = config.get('API.retry_delay', 1)
retry_strategy = Retry(
total=retry_times,
backoff_factor=retry_delay,
status_forcelist=[429, 500, 502, 503, 504],
method_whitelist=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def _build_url(self, endpoint: str) -> str:
"""构建完整URL"""
if endpoint.startswith('http'):
return endpoint
return f"{self.base_url}/{endpoint.lstrip('/')}"
def _log_request(self, method: str, url: str, **kwargs):
"""记录请求日志"""
logger.info(f"发送{method}请求: {url}")
if 'json' in kwargs:
logger.debug(f"请求体: {kwargs['json']}")
if 'params' in kwargs:
logger.debug(f"查询参数: {kwargs['params']}")
def _log_response(self, response: requests.Response, start_time: float):
"""记录响应日志"""
duration = time.time() - start_time
logger.info(f"收到响应: {response.status_code} - {duration:.3f}s")
if response.status_code >= 400:
logger.error(f"请求失败: {response.text[:200]}")
def request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
"""
通用请求方法
Args:
method: HTTP方法
endpoint: 接口端点
**kwargs: 其他请求参数
Returns:
响应对象
"""
url = self._build_url(endpoint)
kwargs.setdefault('timeout', self.timeout)
self._log_request(method, url, **kwargs)
start_time = time.time()
try:
response = self.session.request(method, url, **kwargs)
self._log_response(response, start_time)
return response
except requests.exceptions.RequestException as e:
logger.error(f"请求异常: {e}")
raise
def get(self, endpoint: str, **kwargs) -> requests.Response:
"""GET请求"""
return self.request('GET', endpoint, **kwargs)
def post(self, endpoint: str, **kwargs) -> requests.Response:
"""POST请求"""
return self.request('POST', endpoint, **kwargs)
def put(self, endpoint: str, **kwargs) -> requests.Response:
"""PUT请求"""
return self.request('PUT', endpoint, **kwargs)
def delete(self, endpoint: str, **kwargs) -> requests.Response:
"""DELETE请求"""
return self.request('DELETE', endpoint, **kwargs)
def patch(self, endpoint: str, **kwargs) -> requests.Response:
"""PATCH请求"""
return self.request('PATCH', endpoint, **kwargs)
def close(self):
"""关闭会话"""
self.session.close()
logger.info("HTTP客户端会话已关闭")3. 工具类模块设计
# src/utils/ 目录结构
"""
工具类模块 - 项目的"工具箱"
就像一个装满各种工具的工具箱,每个工具都有特定的用途
"""
utils_module_structure = {
"assertion.py": {
"职责": "增强断言功能",
"特色": "JMESPath支持、链式调用、自定义验证",
"核心类": "EnhancedAssertion"
},
"data_driver.py": {
"职责": "数据驱动测试",
"特色": "Faker集成、模板系统、批量生成",
"核心类": "DataDriver"
},
"environment.py": {
"职责": "环境管理",
"特色": "多环境支持、配置获取、环境切换",
"核心函数": "get_base_url, get_config"
},
"mock_server.py": {
"职责": "Mock服务器",
"特色": "动态路由、响应模拟、规则管理",
"核心类": "MockServer"
},
"performance.py": {
"职责": "性能测试",
"特色": "并发执行、指标收集、报告生成",
"核心类": "PerformanceTester"
},
"log_moudle.py": {
"职责": "日志管理",
"特色": "结构化日志、多输出、级别控制",
"核心类": "Logger"
}
}组件交互机制:让各部分协同工作
1. 依赖注入模式
# src/core/container.py
"""
依赖注入容器 - 组件的"红娘"
负责管理组件之间的依赖关系,让它们能够和谐相处
"""
from typing import Dict, Any, Type, TypeVar
T = TypeVar('T')
class DIContainer:
"""依赖注入容器"""
def __init__(self):
self._services: Dict[str, Any] = {}
self._singletons: Dict[str, Any] = {}
def register(self, name: str, service: Any, singleton: bool = False):
"""注册服务"""
self._services[name] = service
if singleton:
self._singletons[name] = None
def get(self, name: str) -> Any:
"""获取服务实例"""
if name in self._singletons:
if self._singletons[name] is None:
self._singletons[name] = self._create_instance(name)
return self._singletons[name]
return self._create_instance(name)
def _create_instance(self, name: str) -> Any:
"""创建服务实例"""
service = self._services.get(name)
if service is None:
raise ValueError(f"服务未注册: {name}")
if callable(service):
return service()
return service
# 全局容器实例
container = DIContainer()
# 注册核心服务
container.register('config', lambda: config, singleton=True)
container.register('http_client', lambda: BaseClient(config.get('API.base_url')))2. 事件驱动机制
# src/core/events.py
"""
事件总线 - 组件间的"广播电台"
让组件之间可以通过事件进行解耦通信
"""
from typing import Dict, List, Callable, Any
from collections import defaultdict
class EventBus:
"""事件总线"""
def __init__(self):
self._listeners: Dict[str, List[Callable]] = defaultdict(list)
def subscribe(self, event_name: str, callback: Callable):
"""订阅事件"""
self._listeners[event_name].append(callback)
def unsubscribe(self, event_name: str, callback: Callable):
"""取消订阅"""
if callback in self._listeners[event_name]:
self._listeners[event_name].remove(callback)
def publish(self, event_name: str, data: Any = None):
"""发布事件"""
for callback in self._listeners[event_name]:
try:
callback(data)
except Exception as e:
logger.error(f"事件处理异常: {event_name} - {e}")
# 全局事件总线
event_bus = EventBus()
# 事件装饰器
def on_event(event_name: str):
"""事件监听装饰器"""
def decorator(func):
event_bus.subscribe(event_name, func)
return func
return decorator
# 使用示例
@on_event('test_started')
def handle_test_started(test_info):
logger.info(f"测试开始: {test_info}")
@on_event('test_completed')
def handle_test_completed(test_result):
logger.info(f"测试完成: {test_result}")插件机制:让框架具备"进化"能力
1. 插件接口定义
# src/core/plugin.py
"""
插件系统 - 框架的"进化"机制
让框架可以通过插件扩展功能,就像给手机安装APP一样
"""
from abc import ABC, abstractmethod
from typing import Dict, Any
class Plugin(ABC):
"""插件基类"""
@property
@abstractmethod
def name(self) -> str:
"""插件名称"""
pass
@property
@abstractmethod
def version(self) -> str:
"""插件版本"""
pass
@abstractmethod
def initialize(self, config: Dict[str, Any]):
"""插件初始化"""
pass
@abstractmethod
def finalize(self):
"""插件清理"""
pass
class PluginManager:
"""插件管理器"""
def __init__(self):
self._plugins: Dict[str, Plugin] = {}
def register_plugin(self, plugin: Plugin):
"""注册插件"""
plugin.initialize(config._config_data)
self._plugins[plugin.name] = plugin
logger.info(f"插件注册成功: {plugin.name} v{plugin.version}")
def unregister_plugin(self, plugin_name: str):
"""注销插件"""
if plugin_name in self._plugins:
self._plugins[plugin_name].finalize()
del self._plugins[plugin_name]
logger.info(f"插件注销成功: {plugin_name}")
def get_plugin(self, plugin_name: str) -> Plugin:
"""获取插件"""
return self._plugins.get(plugin_name)
def list_plugins(self) -> List[str]:
"""列出所有插件"""
return list(self._plugins.keys())
# 全局插件管理器
plugin_manager = PluginManager()架构优势与设计思考
1. 可扩展性设计
"""
扩展点设计 - 为未来留下空间
就像房子装修时预留插座一样,为将来的扩展做好准备
"""
extension_points = {
"自定义断言": {
"扩展方式": "继承EnhancedAssertion类",
"应用场景": "业务特定的验证逻辑",
"示例": "assert_business_rule()"
},
"自定义客户端": {
"扩展方式": "继承BaseClient类",
"应用场景": "特殊协议或认证方式",
"示例": "GraphQLClient, WebSocketClient"
},
"自定义数据源": {
"扩展方式": "实现DataSource接口",
"应用场景": "从数据库、Excel等加载测试数据",
"示例": "DatabaseDataSource, ExcelDataSource"
},
"自定义报告": {
"扩展方式": "实现Reporter接口",
"应用场景": "生成特定格式的测试报告",
"示例": "JiraReporter, SlackReporter"
}
}2. 性能优化考虑
"""
性能优化策略 - 让框架跑得更快
"""
performance_optimizations = {
"连接池": "复用HTTP连接,减少连接开销",
"并发执行": "pytest-xdist支持,充分利用多核CPU",
"异步支持": "asyncio集成,提高I/O密集型操作效率",
"缓存机制": "配置缓存、响应缓存,减少重复计算",
"懒加载": "按需加载组件,减少启动时间",
"内存管理": "及时释放资源,避免内存泄漏"
}总结
分层架构设计就像建造一座摩天大楼,每一层都有自己的职责,相互配合才能构建出稳固而美观的建筑。通过这篇文章,我们深入了解了:
- 架构理念:SOLID原则的实际应用
- 分层设计:五层架构的职责划分
- 核心组件:配置管理、HTTP客户端、工具类的设计
- 交互机制:依赖注入、事件驱动的实现
- 扩展能力:插件系统、扩展点的设计
这个架构不是一蹴而就的,而是在实际项目中不断演进和优化的结果。它既保证了代码的可维护性,又提供了足够的灵活性来应对各种复杂的测试场景。
下一篇文章,我们将深入HTTP客户端的实现细节,看看它是如何处理各种复杂的网络请求场景的。
