从 0 到 1 搭建接口自动化测试框架:分层架构 + 数据驱动 + 接口依赖编排
手把手教你搭建一个生产级的接口自动化测试框架。
覆盖 200+ 接口,日均 2000+ 用例,回归时间从 4 小时压缩到 15 分钟。
---虽然我觉得很多AI替代了人工实现的框架,但是其基本的思路还是有必要的保留下
一、为什么需要自己搭建框架?
市面上的接口自动化工具很多,比如 Postman、HttpRunner、JMeter。但如果你遇到以下场景,自研框架可能是更好的选择:
| 场景 | 通用工具的问题 | 自研框架的优势 |
|---|---|---|
| 需要支持 RPC / MQ 协议 | 主要面向 HTTP | 适配器模式统一多协议 |
| 需要和业务深度集成 | 脚本能力有限 | Python 原生,灵活度高 |
| 复杂场景编排(幂等、并发、回调) | 难以实现 | 代码编排,想怎么测就怎么测 |
| 需要和 CI/CD 深度集成 | 需要额外工具链 | Pytest 生态,直接集成 |
我的选择:Pytest + Requests 自研分层框架。
二、框架整体架构
api_test_framework/
├── config/ # 配置层(多环境切换)
│ ├── settings.py
│ ├── base.yaml
│ └── dev.yaml
├── core/ # 核心层(协议适配器)
│ ├── client.py # HTTP 客户端封装
│ └── protocol_adapter.py # 协议适配器(HTTP/RPC/MQ 统一入口)
├── utils/ # 工具层
│ ├── logger.py # 日志工具
│ ├── retry.py # 重试机制
│ ├── db.py # 数据库操作
│ ├── data_loader.py # YAML 数据驱动
│ ├── data_factory.py # 数据工厂
│ ├── data_seeder.py # 数据播种器
│ └── flow.py # 流程编排(接口依赖管理)
├── biz/ # 业务层(接口封装)
│ ├── payment.py # 支付业务
│ └── order.py # 订单业务
├── test_cases/ # 测试用例层
│ ├── conftest.py # 全局 fixture
│ ├── payment/
│ │ └── test_transfer.py # 支付测试用例
│ └── order/
│ └── test_order.py # 订单测试用例
├── data/ # 测试数据(YAML)
│ ├── payment/
│ │ └── transfer_data.yaml
│ └── order/
│ └── order_data.yaml
├── mock_server.py # Mock 服务
├── demo.py # 调用入口演示
├── pytest.ini
└── requirements.txt
分层设计原则:每一层只做一件事,下层不知道上层的存在。
三、核心设计详解
3.1 配置层:多环境切换
python
# config/settings.py
class Settings:
"""配置管理 - 单例模式"""
def __init__(self):
env = os.getenv("TEST_ENV", "dev")
self.config = self._load_config(env)
# base.yaml + dev.yaml 深度合并
使用方式:
bash
TEST_ENV=dev pytest test_cases/ # 本地开发
TEST_ENV=test pytest test_cases/ # CI 环境
TEST_ENV=staging pytest test_cases/ # 预发布环境
设计亮点: 基础配置放 base.yaml,每个环境只需覆盖差异部分,不需要重复写。
3.2 协议适配器:统一 HTTP/RPC/MQ 调用
这是框架最核心的设计。业务中有三种通信协议,每种协议调用方式不同。我们的目标是:对外暴露统一的调用接口,内部根据协议类型路由到不同的执行器。
python
# core/protocol_adapter.py
# 统一的请求数据结构
@dataclass
class ProtocolRequest:
protocol: str # "http" | "rpc" | "mq"
method: str = "POST" # HTTP 方法
url: str = "" # HTTP URL
service: str = "" # RPC 服务名
method_name: str = "" # RPC 方法名
topic: str = "" # MQ 主题
body: Any = None
key: str = "" # MQ 消息 key
# 统一的响应数据结构
@dataclass
class ProtocolResponse:
success: bool
data: Optional[Dict] = None
status_code: int = 200
message: str = ""
raw: Any = None
# 抽象执行器(模板方法模式)
class BaseExecutor(ABC):
@abstractmethod
def execute(self, request: ProtocolRequest) -> ProtocolResponse:
"""子类实现具体的协议调用逻辑"""
pass
# HTTP 执行器
class HTTPExecutor(BaseExecutor):
def execute(self, request) -> ProtocolResponse:
url = f"{settings.base_url}{request.url}"
response = self.client.request(request.method, url, json=request.body)
return ProtocolResponse(success=True, data=response)
# RPC 执行器(示意)
class RPCExecutor(BaseExecutor):
def execute(self, request) -> ProtocolResponse:
# 调用 Dubbo 泛化接口
result = dubbo_generic_invoke(request.service, request.method_name, request.body)
return ProtocolResponse(success=True, data=result)
# 协议适配器(门面模式)
class ProtocolAdapter:
def execute(self, request: ProtocolRequest) -> ProtocolResponse:
executor = self._get_executor(request.protocol)
return executor.execute(request)
def _get_executor(self, protocol: str) -> BaseExecutor:
"""根据协议类型获取执行器"""
if protocol == "http":
return HTTPExecutor()
elif protocol == "rpc":
return RPCExecutor()
elif protocol == "mq":
return MQExecutor()
raise ValueError(f"不支持的协议: {protocol}")
调用方式: 不管什么协议,调用方式都一样。
python
# HTTP 请求
adapter.execute(ProtocolRequest(protocol="http", method="POST", url="/api/payment/transfer", body={...}))
# RPC 调用
adapter.execute(ProtocolRequest(protocol="rpc", service="PaymentService", method_name="transfer", body={...}))
# MQ 消息
adapter.execute(ProtocolRequest(protocol="mq", topic="payment.callback", message={...}))
设计模式应用: 适配器模式(Adapter)+ 模板方法模式(Template Method)+ 门面模式(Facade)。
3.3 业务对象层:接口封装为业务方法
python
# biz/payment.py
class PaymentBiz:
def __init__(self, adapter: ProtocolAdapter):
self.adapter = adapter
def transfer(self, from_account, to_account, amount, idempotent_key=None):
body = {"from": from_account, "to": to_account, "amount": amount}
if idempotent_key:
body["idempotent_key"] = idempotent_key
request = ProtocolRequest(protocol="http", method="POST", url="/api/payment/transfer", body=body)
return self.adapter.execute(request)
def pay(self, order_id, amount):
request = ProtocolRequest(protocol="http", method="POST", url="/api/payment/pay", body={"order_id": order_id, "amount": amount})
return self.adapter.execute(request)
def get_balance(self, account_id):
request = ProtocolRequest(protocol="http", method="GET", url=f"/api/account/{account_id}/balance")
return self.adapter.execute(request)
好处: 用例层不关心底层协议,只需要调用 payment_biz.transfer("A", "B", 1000),业务含义清晰。
3.4 数据驱动:YAML + 参数化
测试数据与代码分离,新增用例只需要加数据,不需要改代码。
yaml
# data/payment/transfer_data.yaml
- id: TC001
desc: "正常调拨-小额"
tags: ["p0", "normal"]
params:
from_account: "ACC_A_001"
to_account: "ACC_B_001"
amount: 1000.00
expect:
code: "SUCCESS"
status: "success"
- id: TC002
desc: "余额不足-调拨拒绝"
tags: ["p1", "exception"]
params:
from_account: "ACC_A_002"
to_account: "ACC_B_001"
amount: 999999.00
expect:
code: "BALANCE_NOT_ENOUGH"
status: "failed"
python
# test_cases/payment/test_transfer.py
@parametrize_from_yaml("payment/transfer_data.yaml", keys=["params", "expect"])
@pytest.mark.p0
def test_transfer_normal(self, payment_biz, params, expect):
"""正常调拨流程"""
resp = payment_biz.transfer(**params)
assert resp.data.get("code") == expect["code"], f"返回码异常: {resp.data}"
3.5 用例分级:P0-P4
| 级别 | 场景 | 执行频率 | 耗时 |
|---|---|---|---|
| P0 | 核心链路冒烟 | 每次 MR 必跑 | 5 分钟 |
| P1 | 核心功能回归 | 每天跑 | 10 分钟 |
| P2 | 全量回归 | 每晚跑 | 15 分钟 |
| P3 | 边界/异常场景 | 每周跑 | 30 分钟 |
bash
pytest -m p0 # P0 冒烟
pytest -m "p0 or p1" # 回归
pytest -n 8 # 8 线程全量跑
四、接口依赖怎么处理?(面试高频问题)
这是测试中非常常见的场景:
- 接口 A 创建订单 → 返回
order_id - 接口 B 支付 → 依赖
order_id - 接口 C 查询 → 依赖
order_id
方案一:链式调用(最常用)
python
def test_full_payment_flow(self, order_biz, payment_biz):
# 1. 创建订单 → 提取 order_id
create_resp = order_biz.create_order(user_id="U001", product_id="P001", amount=99.9)
order_id = create_resp.data.get("order_id")
# 2. 支付 → 传入 order_id
pay_resp = payment_biz.pay(order_id=order_id, amount=99.9)
# 3. 查询 → 传入 order_id
query_resp = order_biz.get_order(order_id)
方案二:Fixture 传递(多个用例复用)
python
@pytest.fixture(scope="function")
def created_order(self, order_biz):
resp = order_biz.create_order(...)
order_id = resp.data.get("order_id")
yield order_id # 返回给测试用例
def test_pay(self, created_order, payment_biz):
payment_biz.pay(order_id=created_order, amount=399.80)
def test_query(self, created_order, order_biz):
order_biz.get_order(created_order)
方案三:Flow 流程编排(推荐,框架内置)
我封装了一个 Flow 工具类,自动管理步骤间的依赖数据。
python
from utils.flow import Flow
def test_full_payment_flow(self, order_biz, payment_biz):
flow = Flow()
# 第一步:创建订单
flow.step("创建订单", order_biz.create_order,
user_id="U001", product_id="P001", amount=199.9)
# 第二步:支付 → 自动引用第一步的 order_id
flow.step("发起支付", payment_biz.pay,
order_id=flow["order_id"], # ← Flow 自动提取
amount=199.9)
# 第三步:回调
flow.step("发送回调", payment_biz.send_callback,
order_id=flow["order_id"],
status="SUCCESS")
# 第四步:查询
flow.step("查询订单", order_biz.get_order,
order_id=flow["order_id"])
# 一次性断言所有步骤
flow.assert_all_success()
flow.print_report()
Flow 的核心能力:
| 功能 | 说明 |
|---|---|
flow.step("名称", biz_method, **kwargs) |
执行一步,自动记录日志 |
flow["order_id"] |
自动从之前步骤提取数据 |
flow.assert_all_success() |
断言全部步骤通过 |
flow.print_report() |
打印执行报告 |
flow.failed_steps |
获取失败的步骤 |
Flow 源码实现细节
python
# utils/flow.py
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional, List
from utils.logger import logger
# 单步执行结果数据结构
@dataclass
class StepResult:
name: str # 步骤名称
success: bool # 是否成功
data: Optional[Dict[str, Any]] = None # 接口返回数据
raw: Any = None # 原始响应对象
error: str = "" # 错误信息
核心是 StepResult 数据结构存储每一步结果,Flow 类管理整个流程:
python
class Flow:
def __init__(self):
self._steps: List[StepResult] = [] # 所有步骤
self._data: Dict[str, Any] = {} # 提取出的依赖数据(order_id 等)
def step(self, name: str, func: Callable, **kwargs) -> StepResult:
"""执行一个步骤"""
logger.info("步骤开始: %s | 参数: %s", name, kwargs)
resp = func(**kwargs)
# 自动解析响应
if hasattr(resp, "success") and hasattr(resp, "data"):
# 我们框架的 ProtocolResponse 对象
success = resp.success
data = resp.data if resp.data else {}
elif isinstance(resp, dict):
# 普通 dict 响应
success = resp.get("code") == "SUCCESS"
data = resp
else:
success = True
data = {"result": str(resp)}
result = StepResult(name=name, success=success, data=data, raw=resp)
# 🔥 核心设计:自动提取常见字段到 _data,方便后续步骤引用
# 自动提取 order_id/transfer_id/transaction_id 等
if data:
for key in ("order_id", "transfer_id", "transaction_id",
"account_id", "product_id", "user_id", "status"):
if key in data:
self._data[f"{name}.{key}"] = data[key]
self._data[key] = data[key] # ← 短名称直接引用
logger.info(" 提取依赖: %s = %s", key, data[key])
self._steps.append(result)
return result
关键设计点:自动提取依赖字段 。只要接口返回了 order_id,后续步骤直接用 flow["order_id"] 就能拿到,不用手动存变量。
python
def __getitem__(self, key: str) -> Any:
"""三种查找方式,按优先级:
1. 先找依赖数据字典(我们自动提取过的)
2. 再找步骤名称(返回整个 StepResult)
3. 最后倒序查找所有步骤的 data
"""
if key in self._data:
return self._data[key]
for step in self._steps:
if step.name == key:
return step
for step in reversed(self._steps):
if step.data and key in step.data:
return step.data[key]
logger.warning("依赖数据未找到: %s", key)
return None
查找顺序设计很巧妙:先找自动提取的字段,再找步骤名,最后倒序找所有步骤 。这样 flow["order_id"] 总能拿到最近一步的 order_id,符合直觉。
python
def assert_all_success(self):
"""一次性断言所有步骤全部成功,失败时输出详细报告"""
failed = self.failed_steps
if failed:
logger.error("=" * 50)
logger.error("流程执行失败,以下步骤未通过:")
for f in failed:
logger.error(" - %s: %s", f.name, f.error or f.data)
logger.error("=" * 50)
raise AssertionError(
f"流程执行失败: {len(failed)}/{len(self._steps)} 步骤失败"
)
def print_report(self):
"""打印执行报告,排查问题一目了然"""
logger.info("=" * 50)
logger.info("流程执行报告")
logger.info("=" * 50)
for s in self._steps:
status = "✓" if s.success else "✗"
logger.info(" %s %s", status, s.name)
if s.data:
logger.info(" 数据: %s", s.data)
logger.info("%s/%s 步骤通过", sum(1 for s in self._steps if s.success), len(self._steps))
logger.info("=" * 50)
设计亮点:
- 自动依赖提取 :不用手动存
order_id = create_resp.data.get("order_id"),Flow 自动帮你提取,后续直接引用 - 三种查找方式 :
flow["order_id"]/flow["创建订单"]/flow["创建订单"].order_id,怎么方便怎么来 - 自动日志:每一步都打日志,失败时哪一步出问题一目了然
- 一键断言 :
flow.assert_all_success()一次性断言所有步骤,不用每个步骤都写assert
执行失败后会自动输出完整报告:
==================================================
流程执行失败,以下步骤未通过:
- 发送回调: HTTP 404: Not Found
==================================================
AssertionError: 流程执行失败: 1/4 步骤失败
五、数据依赖怎么处理?
三种数据准备方式
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| API 创建 | 核心链路测试 | 最真实 | 速度慢 |
| SQL 插入 | 批量数据准备 | 速度快 | 绕过业务逻辑 |
| Mock 数据 | 本地开发 | 零依赖 | 不可信 |
DataFactory:自动创建 + 自动清理
python
from utils.data_factory import DataFactory
class TestDataDependency:
def test_create_account_and_transfer(self, data_factory, payment_biz):
# 创建测试账户(自动生成唯一 ID)
account_a = data_factory.create_account(balance=10000.00)
account_b = data_factory.create_account(balance=5000.00)
# 执行调拨
resp = payment_biz.transfer(from_account=account_a.account_id, ...)
# 数据清理在 fixture 中自动执行,无需手动调用
fixture 级别为 function,每个用例独立实例,用例结束后自动清理:
python
@pytest.fixture(scope="function")
def data_factory(adapter, db):
factory = DataFactory(adapter=adapter, db=db)
yield factory
factory.cleanup() # 用例结束自动清理
六、Mock Server:本地调试利器
为了方便本地开发和调试,我写了一个 Mock Server,模拟真实 API 服务。
python
# mock_server.py
class MockHandler(BaseHTTPRequestHandler):
def do_POST(self):
if path == "/api/payment/transfer":
self._handle_transfer(body)
elif path == "/api/order/create":
self._handle_create_order(body)
启动方式:
bash
python mock_server.py
# Mock API 服务已启动: http://127.0.0.1:8766
支持接口:
POST /api/payment/transfer - 资金调拨
POST /api/payment/pay - 支付
POST /api/payment/refund - 退款
GET /api/account/:id/balance - 查询余额
POST /api/order/create - 创建订单
GET /api/order/:id - 查询订单
POST /api/order/cancel - 取消订单
POST /api/admin/reset - 重置数据
七、框架使用指南
7.1 基础运行
bash
# 1. 启动 Mock 服务
python mock_server.py
# 2. 运行测试
pytest test_cases/ -m p0 -v # P0 冒烟
pytest test_cases/ -v # 全量
pytest test_cases/payment/ -v # 支付模块
pytest test_cases/ -n 8 # 8 线程并行
7.2 新增测试用例
方式一:数据驱动(推荐,简单场景)
只需要在 YAML 里加一条数据:
yaml
- id: TC_NEW_001
desc: "新增场景"
tags: ["p1"]
params: {from_account: "A", to_account: "B", amount: 100}
expect: {code: "SUCCESS"}
方式二:代码编排(复杂场景)
python
@pytest.mark.p2
def test_my_new_scenario(self, payment_biz):
"""我的新场景"""
resp = payment_biz.transfer(from_account="A", to_account="B", amount=1000)
assert resp.data.get("code") == "SUCCESS"
7.3 切换真实环境
修改 config/dev.yaml:
yaml
base_url: "http://真实API地址:端口"
database:
host: "数据库地址"
port: 3306
user: "用户名"
password: "密码"
或用环境变量:
bash
TEST_ENV=staging pytest test_cases/ -m p0
八、CI/CD 集成
yaml
# .github/workflows/test.yml
name: API Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: pip install -r requirements.txt
- name: P0 smoke test
run: python -m pytest test_cases/ -m p0 -n 8 --html=report.html
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: test-report
path: report.html
卡点策略:
- MR 创建 → P0 冒烟,不通过不合并
- 合并到 main → P0+P1 回归
- 每晚定时 → 全量回归
- 覆盖率下降 > 1% → 卡住 MR
九、常见问题汇总
Q1:为什么用 Pytest 不用 Unittest?
Pytest 的 fixture 设计更灵活,支持多级 scope(session/module/function);参数化很方便,配合 YAML 数据驱动很自然;插件生态丰富,pytest-xdist 并行、pytest-html 报告开箱即用。而且语法简洁,直接
assert就够了。
Q2:为什么自己写框架,不用现成的?
现成工具(如 HttpRunner)主要面向 HTTP,我们业务需要支持 RPC 和 MQ,扩展起来不如自己写灵活。而且自研框架可以和业务代码深度集成,定制更方便。不是工具不好,适合自己业务的才是最好的。
Q3:怎么处理用例依赖?
原则是用例之间尽量隔离,每个用例自己准备数据、自己清理。fixture 不同 scope 解决不同级别依赖。如果确实有依赖(比如"创建订单→支付→回调"),就编排成一个端到端用例,不分拆成独立用例。
Q4:怎么保证用例不互相影响?
数据隔离:每个用例用
DataFactory生成唯一 ID,fixture 级别为 function,用例结束后自动清理。接口依赖通过Flow工具在同一个用例内编排,不上提到全局依赖。
Q5:框架的覆盖率和效率?
覆盖 200+ 核心接口,2000+ 用例。P0 冒烟 5 分钟,P0+P1 10 分钟,全量跑完 15 分钟。原来手工回归需要 4 小时,效率提升很明显。
Q6:怎么和 CI/CD 集成?
MR 阶段触发 P0 冒烟,不通过不允许合并;合并后触发 P0+P1 回归;每晚全量回归。测试结果自动通知飞书群,生成 HTML 报告。
--
总结
这个框架的核心设计思想是:分层解耦、数据驱动、多协议统一、接口依赖编排。
- 分层架构:配置层→工具层→协议适配层→业务层→用例层,每层职责清晰
- 数据驱动:YAML 管理测试数据,新增用例只加数据不改代码
- 多协议适配:适配器模式统一 HTTP/RPC/MQ 调用
- Flow 编排:链式调用 + 自动依赖解析,管理接口间的数据依赖
- 分级执行:P0-P3 四级,按需选择运行范围,回归时间从 4h → 15min