本文针对纯Excel驱动无法处理复杂数据、纯YAML驱动团队协作门槛高的行业核心痛点,提出**「Excel+YAML双驱动」的企业级最佳实践方案**。该方案通过明确的职责边界划分,实现「技术配置与业务用例完全解耦」:YAML负责结构化配置、复杂请求体模板、技术层参数管理;Excel负责业务用例、入参组合、预期结果、执行开关管理。本文将完整拆解该方案的设计理念、架构分层、全量代码实现与落地规范,兼顾小白可落地性与企业级可扩展性,是目前国内主流互联网、金融、企业级软件公司的自动化落地标准方案。
一、为什么企业级落地必须选Excel+YAML双驱动?
在接口自动化落地过程中,纯Excel与纯YAML驱动均存在无法规避的原生缺陷,而双驱动方案完美互补两者的短板,解决企业级落地的核心痛点。
1.1 纯驱动方案的原生痛点
| 驱动方式 | 核心优势 | 企业级落地的致命短板 |
|---|---|---|
| 纯Excel | 小白零门槛、非技术人员可维护、批量用例填充便捷 | 1. 无法处理嵌套JSON、数组等复杂请求体;2. 二进制文件Git版本控制灾难,无法追溯变更;3. 大文件性能差,格式易损坏;4. 无法管理多环境配置、用例依赖等结构化数据 |
| 纯YAML | 完美支持复杂数据结构、Git版本控制友好、性能强、可管理结构化配置 | 1. 有严格的语法门槛,缩进/符号错误直接报错,小白上手难;2. 非技术人员(产品、手工测试)完全无法参与用例维护;3. 批量用例填充效率低,可视化差 |
1.2 双驱动方案的核心价值
双驱动方案的核心设计思想是**「让专业的工具做专业的事」**,通过明确的职责划分,实现1+1>2的效果:
- 技术与业务完全解耦:测试开发负责维护YAML中的技术配置、请求模板、框架代码;手工测试/业务测试负责维护Excel中的业务用例,互不干扰;
- 兼顾易用性与扩展性:既保留了Excel的低门槛可视化优势,又通过YAML解决了复杂数据结构、版本控制的问题;
- 企业级协作友好:非技术人员无需接触代码/语法,仅需填Excel即可完成用例新增/修改;技术人员无需关注业务用例的频繁变更,专注于框架与模板维护;
- 版本控制可追溯:YAML配置、框架代码纳入Git版本管理,变更清晰可追溯;Excel用例可单独归档,兼顾灵活性与规范性。
二、核心设计:双驱动的职责边界划分(必须严格遵守)
这是双驱动方案落地的核心,必须明确Excel与YAML的职责边界,绝对不能混用,否则会回到维护混乱的老路。
2.1 YAML:负责所有技术层、结构化、固定不变的内容
YAML仅存放技术人员维护、不频繁变更、结构化强的内容,具体包括:
- 多环境全局配置:开发/测试/预发布环境的域名、账号、数据库配置、全局请求头、超时时间等;
- 接口元数据定义:接口地址、请求方式、请求头、鉴权方式等接口固定属性;
- 复杂请求体模板:嵌套JSON、数组等复杂结构的请求体模板,通过变量占位符实现参数化;
- 全局公共参数:全局通用的常量、签名规则、加密方式、用例前置/后置操作等;
- 断言规则模板:通用的断言规则(如响应码校验、JSON结构校验、业务码校验)。
2.2 Excel:负责所有业务层、频繁变更、非技术人员维护的内容
Excel仅存放业务测试人员维护、频繁变更、扁平可视化的内容,具体包括:
- 业务测试用例:用例编号、用例描述、执行开关(是否执行)、用例级别等基础信息;
- 入参变量值:对应YAML请求体模板中的变量,填充具体的测试值;
- 预期结果:预期响应码、预期业务码、预期关键字、预期数据条数等断言条件;
- 用例个性化配置:单条用例的前置/后置操作、重试次数、超时时间等个性化配置。
黄金法则:技术配置绝对不写在Excel里,业务用例绝对不写在YAML里。
三、企业级框架整体架构设计
3.1 完整目录结构(严格遵循)
EnterpriseApiAutoFrame/
├── config/ # 全局配置层(YAML)
│ ├── env_config.yaml # 多环境全局配置
│ ├── api_def.yaml # 接口元数据定义
│ └── request_template.yaml # 复杂请求体模板
├── data/ # 业务数据层
│ ├── business_cases/ # Excel业务用例(按模块划分)
│ │ ├── login_cases.xlsx
│ │ └── order_cases.xlsx
│ └── upload_files/ # 接口上传文件存放目录
├── logs/ # 运行日志(自动生成)
├── reports/ # 测试报告(自动生成)
├── core/ # 核心层
│ ├── __init__.py
│ ├── request_client.py # 统一请求客户端
│ └── template_render.py # 模板渲染引擎(双驱动核心)
├── utils/ # 工具层
│ ├── __init__.py
│ ├── yaml_utils.py # YAML读取工具
│ ├── excel_utils.py # Excel读取工具
│ ├── logger_utils.py # 日志工具
│ ├── sign_utils.py # 加签验签工具
│ └── db_utils.py # 数据库操作工具
├── tests/ # 用例执行层
│ ├── __init__.py
│ ├── conftest.py # pytest全局配置
│ ├── test_login.py # 登录模块用例
│ └── test_order.py # 订单模块用例
├── requirements.txt # 全量依赖包
└── run.py # 一键执行入口3.2 双驱动核心执行流程
1. 框架启动 → 2. 读取YAML配置:环境配置、接口定义、请求体模板
→ 3. 读取Excel业务用例:获取用例入参、预期结果、执行开关
→ 4. 模板渲染:将Excel中的变量值,填充到YAML的请求体模板中,生成完整请求
→ 5. 发起接口请求:通过统一请求客户端执行请求
→ 6. 结果断言:读取Excel中的预期结果,自动执行校验
→ 7. 生成报告:记录全链路日志,生成可视化测试报告四、环境准备与依赖安装
4.1 基础环境
- Python 3.8+(安装时勾选「Add Python to PATH」)
- Java 8+(用于Allure报告生成)
- Allure命令行工具(用于可视化报告)
4.2 全量依赖包
在根目录创建requirements.txt,复制以下内容:
# 核心HTTP请求
requests==2.31.0
# 测试执行框架
pytest==7.4.3
pytest-rerunfailures==12.0
# 可视化报告
allure-pytest==2.13.2
# 配置与数据处理
PyYAML==6.0.1
openpyxl==3.1.2
Jinja2==3.1.2 # 核心:模板渲染引擎,双驱动的关键
# 日志与邮件
loguru==0.7.2
yagmail==0.15.293
# 数据库操作
PyMySQL==1.1.0
# JSON结构校验
jsonschema==4.20.0在根目录打开终端,执行以下命令一键安装:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple五、核心工具层全量实现(双驱动落地的基础)
所有工具类均采用单例模式设计,企业级可直接复用,复制即可使用。
5.1 日志工具 utils/logger_utils.py
全链路日志记录,兼容双驱动场景,按天分割自动清理:
import os
from loguru import logger
from datetime import datetime
class Logger:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "logger"):
log_path = os.path.join(os.getcwd(), "logs")
if not os.path.exists(log_path):
os.makedirs(log_path)
log_file = os.path.join(log_path, f"{datetime.now().strftime('%Y-%m-%d')}.log")
logger.remove()
# 控制台输出
logger.add(
sink=lambda msg: print(msg, end=""),
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {file}:{line} | {message}"
)
# 文件输出
logger.add(
sink=log_file,
level="DEBUG",
rotation="00:00",
retention="15 days",
enqueue=True,
encoding="utf-8",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {file}:{line} | {message}"
)
self.logger = logger
def get_logger(self):
return self.logger
log = Logger().get_logger()5.2 YAML读取工具 utils/yaml_utils.py
双驱动核心工具,负责读取YAML配置、接口定义、请求模板:
import yaml
import os
from utils.logger_utils import log
class YamlUtils:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config")
def read_yaml(self, file_name):
"""
读取YAML文件
:param file_name: config目录下的YAML文件名,如env_config.yaml
:return: 解析后的字典/列表
"""
file_path = os.path.join(self.config_dir, file_name)
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
log.info(f"YAML文件读取成功: {file_name}")
return data
except Exception as e:
log.error(f"YAML文件读取失败: {file_name}, 错误: {str(e)}")
raise
# 全局单例
yaml_utils = YamlUtils()5.3 Excel读取工具 utils/excel_utils.py
业务用例读取工具,支持用例开关、多sheet读取、空行过滤:
import openpyxl
import os
from utils.logger_utils import log
class ExcelUtils:
def __init__(self, file_name):
"""
初始化Excel工具
:param file_name: Excel文件名,需放在data/business_cases/目录下
"""
self.file_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"data",
"business_cases",
file_name
)
def read_excel(self, sheet_name="Sheet1"):
"""
读取Excel业务用例,自动过滤未启用的用例
:param sheet_name: 工作表名,默认Sheet1
:return: 用例字典列表
"""
try:
wb = openpyxl.load_workbook(self.file_path, data_only=True, read_only=True)
if sheet_name not in wb.sheetnames:
log.error(f"Excel工作表不存在: {sheet_name}")
raise ValueError(f"工作表{sheet_name}不存在")
sheet = wb[sheet_name]
headers = [cell.value for cell in sheet[1]]
case_list = []
for row in sheet.iter_rows(min_row=2, values_only=True):
if not any(row):
continue
row_data = dict(zip(headers, row))
# 仅读取is_run=Y的用例
if str(row_data.get("is_run", "Y")).strip().upper() == "Y":
case_list.append(row_data)
wb.close()
log.info(f"Excel用例读取成功: {self.file_path}, 有效用例数: {len(case_list)}")
return case_list
except Exception as e:
log.error(f"Excel读取失败: {str(e)}")
raise5.4 模板渲染引擎 core/template_render.py
双驱动方案的核心灵魂,基于Jinja2实现,将Excel中的变量值渲染到YAML的请求体模板中,解决Excel无法处理复杂嵌套JSON的痛点:
from jinja2 import Template
from utils.logger_utils import log
class TemplateRender:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def render(self, template_content, params):
"""
渲染模板内容
:param template_content: YAML中定义的请求体模板字符串
:param params: Excel中读取的参数字典
:return: 渲染后的完整内容
"""
try:
template = Template(template_content)
render_result = template.render(**params)
log.info(f"模板渲染成功,入参: {params}")
log.debug(f"渲染结果: {render_result}")
return render_result
except Exception as e:
log.error(f"模板渲染失败: {str(e)}")
raise
def render_json(self, template_dict, params):
"""
渲染字典格式的模板(推荐使用)
:param template_dict: YAML中解析的请求体模板字典
:param params: Excel中读取的参数字典
:return: 渲染后的字典
"""
# 将字典转为字符串模板,渲染后再转回字典
template_str = str(template_dict).replace("'", '"')
render_str = self.render(template_str, params)
return eval(render_str)
# 全局单例
template_render = TemplateRender()5.5 统一请求客户端 core/request_client.py
封装requests库,统一处理鉴权、日志、重试、异常捕获,企业级通用:
import requests
import allure
import json
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from utils.logger_utils import log
from utils.yaml_utils import yaml_utils
class RequestClient:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "session"):
# 读取环境配置
self.env_config = yaml_utils.read_yaml("env_config.yaml")
self.base_url = self.env_config["BaseURL"][self.env_config["ENV"]]
self.timeout = self.env_config["Request"]["timeout"]
self.retry_times = self.env_config["Request"]["retry_times"]
# 初始化会话
self.session = requests.Session()
# 重试策略
retry_strategy = Retry(
total=self.retry_times,
backoff_factor=self.env_config["Request"]["retry_delay"],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
status_forcelist=[500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
# 全局默认请求头
self.session.headers.update(self.env_config["Request"]["default_headers"])
def set_token(self, token):
"""设置全局鉴权Token"""
self.session.headers.update({"Authorization": f"Bearer {token}"})
log.info("全局Token已设置")
def clear_token(self):
"""清除全局Token"""
self.session.headers.pop("Authorization", None)
log.info("全局Token已清除")
def _handle_url(self, url):
"""拼接基础域名"""
if url.startswith(("http://", "https://")):
return url
return f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
def request(self, method, url, api_name="", **kwargs):
"""统一请求入口"""
request_url = self._handle_url(url)
log.info(f"==================== 接口开始: {api_name} ====================")
log.info(f"请求方式: {method.upper()} | 请求地址: {request_url}")
# 记录请求详情
if kwargs.get("params"):
log.info(f"请求参数: {json.dumps(kwargs['params'], ensure_ascii=False)}")
if kwargs.get("json"):
log.info(f"请求体: {json.dumps(kwargs['json'], ensure_ascii=False)}")
if kwargs.get("data"):
log.info(f"表单数据: {kwargs['data']}")
try:
# 发送请求
response = self.session.request(
method=method.upper(),
url=request_url,
timeout=self.timeout,
**kwargs
)
# 记录响应
log.info(f"响应状态码: {response.status_code}")
log.info(f"响应内容: {response.text}")
log.info(f"==================== 接口结束: {api_name} ====================")
# 写入Allure报告
with allure.step(f"接口执行: {api_name}"):
allure.attach(f"{method.upper()} {request_url}", name="请求地址", attachment_type=allure.attachment_type.TEXT)
if kwargs.get("params"):
allure.attach(json.dumps(kwargs['params'], ensure_ascii=False, indent=2), name="请求参数", attachment_type=allure.attachment_type.JSON)
if kwargs.get("json"):
allure.attach(json.dumps(kwargs['json'], ensure_ascii=False, indent=2), name="请求体", attachment_type=allure.attachment_type.JSON)
allure.attach(str(response.status_code), name="响应状态码", attachment_type=allure.attachment_type.TEXT)
allure.attach(response.text, name="响应内容", attachment_type=allure.attachment_type.JSON)
return response
except Exception as e:
log.error(f"接口请求异常: {str(e)}")
log.info(f"==================== 接口结束: {api_name} ====================")
raise
# 快捷请求方法
def get(self, url, api_name="", **kwargs):
return self.request("GET", url, api_name, **kwargs)
def post(self, url, api_name="", **kwargs):
return self.request("POST", url, api_name, **kwargs)
def put(self, url, api_name="", **kwargs):
return self.request("PUT", url, api_name, **kwargs)
def delete(self, url, api_name="", **kwargs):
return self.request("DELETE", url, api_name, **kwargs)
# 全局单例
request_client = RequestClient()六、YAML配置定义(技术人员维护)
按照职责划分,所有技术层配置均放在config/目录下的YAML文件中,以下为企业级标准示例。
6.1 多环境全局配置 config/env_config.yaml
管理多环境切换、全局请求配置、账号信息等,无需修改代码即可切换环境:
# 当前运行环境,支持dev/test/pre
ENV: test
# 各环境基础域名
BaseURL:
dev: "http://dev-api.example.com"
test: "https://test-api.example.com"
pre: "https://pre-api.example.com"
# 全局请求配置
Request:
timeout: 10
retry_times: 2
retry_delay: 1
default_headers:
Content-Type: "application/json;charset=UTF-8"
User-Agent: "EnterpriseApiAutoFrame/1.0"
# 测试账号配置
TestAccount:
normal_user: "test_user"
normal_pwd: "Test@123456"
admin_user: "admin"
admin_pwd: "Admin@123456"
# 数据库配置
MySQL:
host: "127.0.0.1"
port: 3306
user: "root"
password: "123456"
database: "test_db"
charset: "utf8mb4"
# 签名配置
Sign:
secret_key: "your_sign_secret_key"
sign_type: "md5"6.2 接口元数据定义 config/api_def.yaml
统一管理所有接口的固定属性,避免在代码中硬编码接口地址:
# 用户模块接口
user:
login:
url: "/api/user/login"
method: "POST"
name: "用户登录接口"
get_info:
url: "/api/user/info"
method: "GET"
name: "用户信息查询接口"
# 订单模块接口
order:
create:
url: "/api/order/create"
method: "POST"
name: "订单创建接口"
list:
url: "/api/order/list"
method: "GET"
name: "订单列表查询接口"
detail:
url: "/api/order/detail"
method: "GET"
name: "订单详情查询接口"
# 商品模块接口
goods:
list:
url: "/api/goods/list"
method: "GET"
name: "商品列表查询接口"6.3 复杂请求体模板 config/request_template.yaml
双驱动的核心,定义复杂嵌套请求体模板,通过{{ 变量名 }}设置占位符,与Excel中的参数一一对应:
# 订单创建接口请求体模板(企业级复杂嵌套结构)
order_create_template:
orderNo: "{{ order_no }}"
orderType: "{{ order_type | default(1) }}"
buyerInfo:
buyerName: "{{ buyer_name }}"
buyerPhone: "{{ buyer_phone }}"
buyerAddress: "{{ buyer_address }}"
goodsList:
- goodsId: "{{ goods_id }}"
goodsName: "{{ goods_name }}"
price: "{{ price }}"
quantity: "{{ quantity }}"
payInfo:
payType: "{{ pay_type | default(1) }}"
payAmount: "{{ pay_amount }}"
remark: "{{ remark | default('') }}"
# 用户注册接口请求体模板
user_register_template:
username: "{{ username }}"
password: "{{ password }}"
phone: "{{ phone }}"
email: "{{ email | default('') }}"七、Excel业务用例设计(非技术人员维护)
按照职责划分,所有业务用例均放在data/business_cases/目录下的Excel文件中,以下为企业级标准模板,非技术人员可直接填写。
7.1 订单模块用例模板 data/business_cases/order_cases.xlsx
以企业最常见的订单创建接口为例,完美适配YAML中的请求体模板,Excel中的列名与YAML模板中的变量名一一对应。
| case_id | case_desc | is_run | case_level | order_no | order_type | buyer_name | buyer_phone | buyer_address | goods_id | goods_name | price | quantity | pay_type | pay_amount | remark | expect_code | expect_biz_code | expect_keyword |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 正常创建实物订单 | Y | P0 | OD2024001 | 1 | 张三 | 13800138000 | 北京市朝阳区 | 1001 | 测试商品 | 99.99 | 1 | 1 | 99.99 | 测试订单 | 200 | 0 | 订单创建成功 |
| 2 | 订单号为空创建失败 | Y | P1 | 1 | 张三 | 13800138000 | 北京市朝阳区 | 1001 | 测试商品 | 99.99 | 1 | 1 | 99.99 | 测试订单 | 200 | 400 | 订单号不能为空 | |
| 3 | 商品数量为0创建失败 | Y | P1 | OD2024002 | 1 | 李四 | 13900139000 | 上海市浦东新区 | 1001 | 测试商品 | 99.99 | 0 | 1 | 99.99 | 测试订单 | 200 | 400 | 商品数量必须大于0 |
| 4 | 虚拟商品订单创建 | Y | P0 | OD2024003 | 2 | 王五 | 13700137000 | 广州市天河区 | 1002 | 虚拟商品 | 19.99 | 1 | 2 | 19.99 | 虚拟商品 | 200 | 0 | 订单创建成功 |
| 5 | 临时跳过的用例 | N | P2 | OD2024004 | 1 | 赵六 | 13600136000 | 深圳市南山区 | 1003 | 测试商品3 | 29.99 | 1 | 1 | 29.99 | 测试 | 200 | 0 | 订单创建成功 |
模板设计规范:
- 固定列:前4列(case_id、case_desc、is_run、case_level)为通用固定列,所有模块用例必须包含;
- 参数列:
order_no、buyer_name等列,与YAML请求体模板中的{{ 变量名 }}完全一致,一一对应; - 断言列:最后4列为断言列,定义用例的预期结果,支持扩展;
- 默认值处理:非必填参数可空,YAML模板中通过
| default('')设置默认值,无需在Excel中重复填写。
八、测试用例落地实现(双驱动核心串联)
以订单创建接口为例,实现YAML+Excel双驱动的测试用例,代码极简,完全解耦。
8.1 pytest全局配置 tests/conftest.py
实现全局鉴权、环境配置、生命周期管理:
import pytest
from utils.logger_utils import log
from utils.yaml_utils import yaml_utils
from core.request_client import request_client
# 全局重试配置
RERUN_TIMES = yaml_utils.read_yaml("env_config.yaml")["Request"]["retry_times"]
RERUN_DELAY = yaml_utils.read_yaml("env_config.yaml")["Request"]["retry_delay"]
def pytest_addoption(parser):
"""支持命令行切换环境"""
parser.addoption("--env", action="store", default="test", help="运行环境: dev/test/pre")
@pytest.fixture(scope="session", autouse=True)
def init_env(request):
"""全局环境初始化,整个测试会话执行一次"""
env = request.config.getoption("--env")
log.info(f"当前测试环境: {env}")
yield
@pytest.fixture(scope="session")
def get_login_token():
"""全局登录获取Token,所有需要鉴权的用例共用"""
# 读取接口定义与账号配置
api_def = yaml_utils.read_yaml("api_def.yaml")
account = yaml_utils.read_yaml("env_config.yaml")["TestAccount"]
# 调用登录接口
login_api = api_def["user"]["login"]
response = request_client.post(
url=login_api["url"],
api_name=login_api["name"],
json={
"username": account["normal_user"],
"password": account["normal_pwd"]
}
)
# 解析Token
token = response.json().get("data", {}).get("token")
assert token is not None, "登录获取Token失败"
# 设置全局Token
request_client.set_token(token)
log.info("全局鉴权Token设置成功")
yield token
# 测试结束后清除Token
request_client.clear_token()
@pytest.fixture(scope="function", autouse=True)
def case_lifecycle():
"""用例生命周期钩子"""
log.info("-------------------- 用例开始执行 --------------------")
yield
log.info("-------------------- 用例执行结束 --------------------")8.2 订单模块测试用例 tests/test_order.py
双驱动核心用例,代码极简,仅需串联YAML模板与Excel用例,无需硬编码任何参数:
import allure
import pytest
from utils.yaml_utils import yaml_utils
from utils.excel_utils import ExcelUtils
from core.request_client import request_client
from core.template_render import template_render
# ==================== 双驱动数据读取 ====================
# 1. 读取YAML配置:接口定义、请求体模板
api_def = yaml_utils.read_yaml("api_def.yaml")
request_template = yaml_utils.read_yaml("request_template.yaml")
# 2. 读取Excel业务用例
order_cases = ExcelUtils("order_cases.xlsx").read_excel()
# ==================== 测试用例集 ====================
@allure.feature("订单模块")
@pytest.mark.usefixtures("get_login_token") # 自动注入登录Token
class TestOrder:
@allure.story("订单创建接口")
@allure.title("用例{case[case_id]}:{case[case_desc]}")
@pytest.mark.parametrize("case", order_cases)
def test_order_create(self, case):
"""
订单创建接口-Excel+YAML双驱动测试用例
无需修改代码,仅需修改Excel即可新增/修改用例
"""
# 1. 获取接口定义与模板
order_api = api_def["order"]["create"]
template = request_template["order_create_template"]
# 2. 核心:模板渲染,将Excel参数填充到YAML模板中
request_body = template_render.render_json(template, case)
# 3. 发起接口请求
response = request_client.request(
method=order_api["method"],
url=order_api["url"],
api_name=order_api["name"],
json=request_body
)
response_data = response.json()
# 4. 读取Excel中的预期结果,自动断言
# 断言1:HTTP响应码
expect_code = case.get("expect_code")
if expect_code:
assert response.status_code == int(expect_code), \
f"响应码不符合预期!预期:{expect_code},实际:{response.status_code}"
# 断言2:业务响应码
expect_biz_code = case.get("expect_biz_code")
if expect_biz_code:
assert response_data.get("code") == int(expect_biz_code), \
f"业务码不符合预期!预期:{expect_biz_code},实际:{response_data.get('code')}"
# 断言3:响应关键字
expect_keyword = case.get("expect_keyword")
if expect_keyword:
assert expect_keyword in response.text, \
f"响应内容不符合预期!预期包含:{expect_keyword},实际响应:{response.text}"8.3 一键执行入口 run.py
全流程自动化执行,清理旧报告、执行用例、生成报告、邮件通知:
import os
import shutil
import pytest
from datetime import datetime
from utils.yaml_utils import yaml_utils
from utils.logger_utils import log
def clean_old_report():
"""清理旧报告数据"""
log.info("开始清理旧报告数据")
allure_results = os.path.join(os.getcwd(), "reports", "allure-results")
allure_report = os.path.join(os.getcwd(), "reports", "allure-report")
for dir_path in [allure_results, allure_report]:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
os.makedirs(dir_path, exist_ok=True)
log.info("旧报告清理完成")
def run_test_cases():
"""执行测试用例"""
log.info("开始执行接口自动化测试")
env_config = yaml_utils.read_yaml("env_config.yaml")
pytest_args = [
"tests/",
"-v",
"-s",
f"--reruns={env_config['Request']['retry_times']}",
f"--reruns-delay={env_config['Request']['retry_delay']}",
"--alluredir=reports/allure-results",
"--clean-alluredir"
]
result = pytest.main(pytest_args)
log.info(f"测试执行完成,结果码: {result}")
return result
def open_report():
"""打开Allure可视化报告"""
log.info("正在生成测试报告")
os.system("allure serve reports/allure-results")
if __name__ == "__main__":
clean_old_report()
run_result = run_test_cases()
open_report()九、企业级最佳实践与避坑指南
9.1 双驱动落地的5条铁律
- 职责边界绝对不能破:技术配置绝对不写在Excel里,业务用例绝对不写在YAML里,否则会回到维护混乱的老路;
- 模板与参数一一对应:YAML模板中的变量名,必须与Excel中的列名完全一致,避免大小写、拼写错误;
- 非必填参数必须设置默认值:YAML模板中通过
{{ 变量名 | default('默认值') }}设置默认值,避免Excel空值导致的渲染错误; - 用例分级管理:Excel中通过
case_level标记用例级别(P0/P1/P2),支持冒烟测试、全量测试的分级执行; - 版本控制规范:YAML配置、框架代码必须纳入Git管理;Excel用例按版本归档,避免多人同时修改导致的冲突。
9.2 常见避坑指南
- YAML语法错误:YAML对缩进极其敏感,必须用空格缩进,不能用Tab,键值对后面必须加冒号;
- Excel列名拼写错误:Excel中的列名必须与YAML模板中的变量名完全一致,包括大小写,否则会渲染失败;
- Excel文件被占用:执行用例前必须关闭Excel文件,否则代码会因为文件被锁定而读取失败;
- 复杂模板渲染异常:嵌套层级过深的模板,建议拆分多个子模板,避免渲染失败;
- 用例依赖问题:避免Excel用例之间的强依赖,单条用例必须可独立执行,前置操作通过Fixture实现。
9.3 扩展能力
该框架可无缝扩展以下企业级能力:
- 数据库断言:通过
db_utils.py实现接口执行结果的数据库级校验; - 接口加签验签:通过
sign_utils.py实现接口请求的自动加签; - CI/CD集成:无缝接入Jenkins、GitLab CI等平台,实现代码提交后自动触发测试;
- 邮件/企业微信/钉钉通知:执行完成后自动推送测试结果到企业通讯工具;
- 接口性能测试:复用接口定义与模板,基于locust快速实现接口性能压测。
Excel+YAML双驱动方案,是目前企业级接口自动化测试落地的最佳实践,完美解决了纯Excel与纯YAML驱动的原生痛点。它既通过Excel降低了业务用例的维护门槛,让非技术人员也能参与自动化建设;又通过YAML解决了复杂数据结构、版本控制的问题,满足了企业级场景的扩展性需求。
本文提供的框架代码与设计规范,可直接复制落地到企业级项目中,无需大量二次开发,即可快速搭建一套规范、可维护、可扩展的接口自动化测试体系。








