4. 项目结构设计:目录规范、模块划分、配置文件管理、日志系统设计
好,我们开始聊项目结构。说实话,很多自动化测试项目做着做着就烂掉了,根因往往不是技术选型不对,而是从一开始目录就没规划好。我见过太多项目,测试脚本和配置文件混在一起,日志满天飞,最后连自己写的用例都找不着。嗯,这一章我们就来把地基打牢。
4.1 目录规范:让每个人都能一眼看懂
我个人习惯,项目根目录下只放三样东西:源码、配置、文档。其他编译产物、临时文件统统隔离出去。下面是我在多个座舱项目中沉淀下来的目录模板:
auto_test_framework/
├── src/ # 源码主目录
│ ├── core/ # 核心引擎
│ │ ├── engine.py # 测试执行引擎
│ │ ├── runner.py # 用例调度器
│ │ └── reporter.py # 报告生成器
│ ├── libs/ # 公共库
│ │ ├── adb_utils.py # ADB 工具封装
│ │ ├── can_utils.py # CAN 总线工具
│ │ └── logger.py # 日志封装
│ ├── pages/ # 页面对象模型
│ │ ├── home_page.py
│ │ ├── settings_page.py
│ │ └── media_page.py
│ ├── tests/ # 测试用例
│ │ ├── smoke/ # 冒烟测试
│ │ ├── functional/ # 功能测试
│ │ └── stress/ # 压力测试
│ └── utils/ # 工具函数
│ ├── data_parser.py
│ └── screenshot.py
├── config/ # 配置文件
│ ├── env.yaml # 环境配置
│ ├── devices.yaml # 设备配置
│ └── test_config.yaml # 测试参数
├── data/ # 测试数据
│ ├── input/ # 输入数据
│ └── output/ # 输出报告
├── logs/ # 日志目录
├── docs/ # 文档
├── requirements.txt
└── README.md
核心原则:按职责划分,不按文件类型划分。不要把所有的 .py 文件扔到一个目录里,那叫垃圾桶,不叫项目结构。
4.2 模块划分:高内聚、低耦合
模块划分说白了就是「谁该管什么事」。我习惯把整个框架拆成四个层次:
| 层次 | 模块 | 职责 | 依赖 |
|---|---|---|---|
| L1 基础设施 | core/ | 引擎、调度、报告 | 无 |
| L2 公共能力 | libs/, utils/ | ADB、CAN、日志、截图 | L1 |
| L3 业务抽象 | pages/ | 页面对象、操作封装 | L2 |
| L4 测试用例 | tests/ | 具体测试场景 | L3 |
你想想看,如果底层引擎改了,上层用例不需要动;如果页面元素变了,只需要改 pages/ 里的文件。这就是分层的好处。我在项目中遇到过最头疼的事,就是有人把 ADB 命令直接写在用例里,结果换了台设备,所有用例都得重写。嗯,血的教训。
4.3 配置文件管理:YAML 还是 JSON?
这个问题几乎每次培训都有人问。我的答案是:YAML 用于人工编辑,JSON 用于程序交互。
为什么?YAML 可读性好,支持注释,适合写环境配置、设备参数这些需要人经常看的东西。JSON 解析快,结构严格,适合做接口传参和数据交换。
来看一个实际的 YAML 配置文件例子:
# config/env.yaml
environment:
name: "test_lab_01"
location: "上海嘉定"
device_pool:
- id: "device_001"
serial: "ZY3229G8H7"
platform: "android"
version: "12.0"
resolution: "1920x1080"
- id: "device_002"
serial: "ZY3229G8H8"
platform: "android"
version: "13.0"
resolution: "2560x1440"
test_params:
timeout: 30
retry_count: 3
screenshot_on_failure: true
小技巧:YAML 里缩进必须一致,别混用空格和 Tab。我习惯用 2 个空格缩进,团队统一就好。另外,敏感信息(密码、token)不要硬编码在 YAML 里,用环境变量注入。
那什么时候用 JSON?比如测试数据和预期结果的映射,或者 CI/CD 流水线传参:
{
"test_suite": "smoke",
"devices": ["device_001", "device_002"],
"report_format": "html",
"email_notify": ["qa@company.com"]
}
4.4 日志系统设计:别等出问题了才后悔
日志这东西,平时没人关心,一出问题它就是救命稻草。我曾经排查过一个偶现的蓝牙断连问题,没有日志根本无从下手。所以日志系统从一开始就要设计好。
我一般用 Python 的 logging 模块,但会做一层封装:
# src/libs/logger.py
import logging
import os
from logging.handlers import RotatingFileHandler
class TestLogger:
def __init__(self, name, log_dir="logs"):
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.DEBUG)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_format = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(console_format)
# 文件输出,按大小轮转
os.makedirs(log_dir, exist_ok=True)
file_handler = RotatingFileHandler(
filename=f"{log_dir}/{name}.log",
maxBytes=10*1024*1024, # 10MB
backupCount=5,
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_format = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
file_handler.setFormatter(file_format)
self.logger.addHandler(console_handler)
self.logger.addHandler(file_handler)
def get_logger(self):
return self.logger
注意:日志级别要区分场景。调试阶段用 DEBUG,跑回归用 INFO,出问题用 ERROR。别一股脑全打 DEBUG,日志文件会爆炸。我见过一个项目一天产生 50GB 日志,硬盘直接写满。
日志格式里我建议带上文件名和行号,这样定位问题快。另外,座舱测试经常涉及多设备并行,每条日志最好带上设备 ID:
2025-03-15 14:23:45 - device_001 - INFO - 开始执行: 蓝牙连接测试
2025-03-15 14:23:46 - device_001 - DEBUG - 发送AT指令: AT+BTCONNECT
2025-03-15 14:23:47 - device_001 - ERROR - 连接超时,重试第1次
4.5 整体结构图
下面这张图把上面讲的内容串起来,你一看就明白:
4.6 避坑指南
最后分享几个我踩过的坑:
- 目录层级别太深。 我曾经见过一个项目嵌套了 7 层目录,找个文件要翻半天。建议不超过 4 层。
- 配置文件不要放在 src 里。 否则每次打包都要重新配置,环境隔离形同虚设。
- 日志一定要轮转。 不轮转的话,跑一个月测试,日志文件能上 TB 级。
- 模块之间不要循环引用。 比如 pages 引用了 utils,utils 又引用了 pages,这种问题排查起来很痛苦。
我的习惯:项目初期花 30 分钟把目录结构定好,写个 README 说明每个目录的用途。后面所有人按规矩来,省心省力。别等到项目中期再重构,那代价太大了。