企业级接口自动化测试框架落地指南:Excel+YAML双驱动最佳实践

本文针对纯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的效果:

  1. 技术与业务完全解耦:测试开发负责维护YAML中的技术配置、请求模板、框架代码;手工测试/业务测试负责维护Excel中的业务用例,互不干扰;
  2. 兼顾易用性与扩展性:既保留了Excel的低门槛可视化优势,又通过YAML解决了复杂数据结构、版本控制的问题;
  3. 企业级协作友好:非技术人员无需接触代码/语法,仅需填Excel即可完成用例新增/修改;技术人员无需关注业务用例的频繁变更,专注于框架与模板维护;
  4. 版本控制可追溯:YAML配置、框架代码纳入Git版本管理,变更清晰可追溯;Excel用例可单独归档,兼顾灵活性与规范性。

二、核心设计:双驱动的职责边界划分(必须严格遵守)

这是双驱动方案落地的核心,必须明确Excel与YAML的职责边界,绝对不能混用,否则会回到维护混乱的老路。

2.1 YAML:负责所有技术层、结构化、固定不变的内容

YAML仅存放技术人员维护、不频繁变更、结构化强的内容,具体包括:

  1. 多环境全局配置:开发/测试/预发布环境的域名、账号、数据库配置、全局请求头、超时时间等;
  2. 接口元数据定义:接口地址、请求方式、请求头、鉴权方式等接口固定属性;
  3. 复杂请求体模板:嵌套JSON、数组等复杂结构的请求体模板,通过变量占位符实现参数化;
  4. 全局公共参数:全局通用的常量、签名规则、加密方式、用例前置/后置操作等;
  5. 断言规则模板:通用的断言规则(如响应码校验、JSON结构校验、业务码校验)。

2.2 Excel:负责所有业务层、频繁变更、非技术人员维护的内容

Excel仅存放业务测试人员维护、频繁变更、扁平可视化的内容,具体包括:

  1. 业务测试用例:用例编号、用例描述、执行开关(是否执行)、用例级别等基础信息;
  2. 入参变量值:对应YAML请求体模板中的变量,填充具体的测试值;
  3. 预期结果:预期响应码、预期业务码、预期关键字、预期数据条数等断言条件;
  4. 用例个性化配置:单条用例的前置/后置操作、重试次数、超时时间等个性化配置。

黄金法则:技术配置绝对不写在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)}")
            raise

5.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_idcase_descis_runcase_levelorder_noorder_typebuyer_namebuyer_phonebuyer_addressgoods_idgoods_namepricequantitypay_typepay_amountremarkexpect_codeexpect_biz_codeexpect_keyword
1正常创建实物订单YP0OD20240011张三13800138000北京市朝阳区1001测试商品99.991199.99测试订单2000订单创建成功
2订单号为空创建失败YP1 1张三13800138000北京市朝阳区1001测试商品99.991199.99测试订单200400订单号不能为空
3商品数量为0创建失败YP1OD20240021李四13900139000上海市浦东新区1001测试商品99.990199.99测试订单200400商品数量必须大于0
4虚拟商品订单创建YP0OD20240032王五13700137000广州市天河区1002虚拟商品19.991219.99虚拟商品2000订单创建成功
5临时跳过的用例NP2OD20240041赵六13600136000深圳市南山区1003测试商品329.991129.99测试2000订单创建成功

模板设计规范:

  1. 固定列:前4列(case_id、case_desc、is_run、case_level)为通用固定列,所有模块用例必须包含;
  2. 参数列order_nobuyer_name等列,与YAML请求体模板中的{{ 变量名 }}完全一致,一一对应;
  3. 断言列:最后4列为断言列,定义用例的预期结果,支持扩展;
  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条铁律

  1. 职责边界绝对不能破:技术配置绝对不写在Excel里,业务用例绝对不写在YAML里,否则会回到维护混乱的老路;
  2. 模板与参数一一对应:YAML模板中的变量名,必须与Excel中的列名完全一致,避免大小写、拼写错误;
  3. 非必填参数必须设置默认值:YAML模板中通过{{ 变量名 | default('默认值') }}设置默认值,避免Excel空值导致的渲染错误;
  4. 用例分级管理:Excel中通过case_level标记用例级别(P0/P1/P2),支持冒烟测试、全量测试的分级执行;
  5. 版本控制规范:YAML配置、框架代码必须纳入Git管理;Excel用例按版本归档,避免多人同时修改导致的冲突。

9.2 常见避坑指南

  1. YAML语法错误:YAML对缩进极其敏感,必须用空格缩进,不能用Tab,键值对后面必须加冒号;
  2. Excel列名拼写错误:Excel中的列名必须与YAML模板中的变量名完全一致,包括大小写,否则会渲染失败;
  3. Excel文件被占用:执行用例前必须关闭Excel文件,否则代码会因为文件被锁定而读取失败;
  4. 复杂模板渲染异常:嵌套层级过深的模板,建议拆分多个子模板,避免渲染失败;
  5. 用例依赖问题:避免Excel用例之间的强依赖,单条用例必须可独立执行,前置操作通过Fixture实现。

9.3 扩展能力

该框架可无缝扩展以下企业级能力:

  1. 数据库断言:通过db_utils.py实现接口执行结果的数据库级校验;
  2. 接口加签验签:通过sign_utils.py实现接口请求的自动加签;
  3. CI/CD集成:无缝接入Jenkins、GitLab CI等平台,实现代码提交后自动触发测试;
  4. 邮件/企业微信/钉钉通知:执行完成后自动推送测试结果到企业通讯工具;
  5. 接口性能测试:复用接口定义与模板,基于locust快速实现接口性能压测。

Excel+YAML双驱动方案,是目前企业级接口自动化测试落地的最佳实践,完美解决了纯Excel与纯YAML驱动的原生痛点。它既通过Excel降低了业务用例的维护门槛,让非技术人员也能参与自动化建设;又通过YAML解决了复杂数据结构、版本控制的问题,满足了企业级场景的扩展性需求。

本文提供的框架代码与设计规范,可直接复制落地到企业级项目中,无需大量二次开发,即可快速搭建一套规范、可维护、可扩展的接口自动化测试体系。

THE END
喜欢就支持一下吧
赞赏 分享