从 0 到 1 搭建接口自动化测试框架:分层架构 + 数据驱动 + 接口依赖编排

从 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)

设计亮点:

  1. 自动依赖提取 :不用手动存 order_id = create_resp.data.get("order_id"),Flow 自动帮你提取,后续直接引用
  2. 三种查找方式flow["order_id"] / flow["创建订单"] / flow["创建订单"].order_id,怎么方便怎么来
  3. 自动日志:每一步都打日志,失败时哪一步出问题一目了然
  4. 一键断言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
相关推荐
Andya_net1 小时前
Spring Boot | 条件注解完全指南:从 @Conditional 到 @ConditionalOnExpression 的原理、实践与避坑
spring boot·后端·python
萧瑟余晖1 小时前
ORM 通用原理与阻抗失配详解
架构
weixin_440730501 小时前
playwright浏览器自动化实战笔记3-登陆以及退出登陆流程-多用户操作
笔记·python·自动化
卷无止境2 小时前
测试全绿,功能能跑,代码却烂到没法上线:AI编程助手留下的十个坑
后端·python
卷无止境2 小时前
终端里的AI战争,命令行编程代理全景扫描
python·agent
SamChan902 小时前
PDF多语言翻译的格式还原技术拆解:从版面分析到内容重排的工程实现
python·ai·pdf·机器翻译
156002548402 小时前
基于3U VPX总线架构的VU37P FPGA高带宽HBM缓存数据处理卡(缓存带宽480GB/s)
fpga开发·架构
liuze4082 小时前
安装boss-zhipin-mcp(招聘)
开发语言·python
TickDB2 小时前
Python 接入 A 股盘前集合竞价数据:AkShare 报错到 TickDB 实测的完整解法
python·websocket·行情数据 api