【接口自动化】13 - 接口数据驱动与报告

前言

第 10 篇用了 JSON 文件做数据驱动,Schema 校验也只在几条用例里手动调用。本篇把这两件事系统化,同时接入 Allure 报告,让接口自动化的产出能被团队其他人看懂。

本篇五件事:

  1. YAML 数据驱动:把 JSON 测试数据迁移到 YAML,对比优劣
  2. Excel 数据驱动:用 openpyxl 读 Excel,适合非技术人员维护数据
  3. Schema 自动校验进阶:conftest 中统一校验 + 从响应自动生成 Schema
  4. Allure 报告集成:请求/响应自动附加、Step 装饰器、用例分类、严重级别
  5. 接口自动化篇总结:完整项目结构、最佳实践、常见坑

一、YAML 数据驱动

1.1 为什么用 YAML

第 10 篇用 JSON 文件做数据驱动。JSON 有两个不方便的地方:

json 复制代码
// JSON 的问题 1:不能写注释
// 想解释"为什么这个用户密码是 wrong"?没法写
{
  "username": "admin",
  "password": "wrong"
}
yaml 复制代码
# YAML 可以写注释
username: admin
password: wrong  # 故意写错密码,测试登录失败场景
json 复制代码
// JSON 的问题 2:括号嵌套深了很难读
{"case_name": "密码错误", "username": "admin", "password": "wrong", "expected_code": 401, "expected_msg": "密码错误"}
yaml 复制代码
# YAML 更易读
case_name: 密码错误
username: admin
password: wrong       # 故意错误
expected_code: 401
expected_msg: 密码错误

1.2 安装依赖

第 10 篇的 requirements.txt 已经加了 pyyaml,确认已安装:

powershell 复制代码
pip install pyyaml

1.3 创建 YAML 数据文件

【新建文件】 api/test_data/cart_cases.yaml

yaml 复制代码
# 购物车测试数据
# 用 YAML 格式,支持注释

add_cases:
  - case_name: 添加单个商品
    product_id: 1
    quantity: 1
    expected_success: true

  - case_name: 添加多个数量
    product_id: 1
    quantity: 5
    expected_success: true

  - case_name: 添加不存在的商品
    product_id: 999999
    quantity: 1
    expected_success: false   # 商品不存在,应该失败

  - case_name: 数量为零
    product_id: 1
    quantity: 0
    expected_success: true    # MallLite 不校验数量,仍然返回成功

  - case_name: 负数数量
    product_id: 1
    quantity: -1
    expected_success: true    # MallLite 不校验数量,仍然返回成功

update_cases:
  - case_name: 修改为更大数量
    product_id: 1
    quantity: 10
    expected_success: true

  - case_name: 修改为1
    product_id: 1
    quantity: 1
    expected_success: true

delete_cases:
  - case_name: 删除已添加的商品
    product_id: 1
    expected_success: true

  - case_name: 删除不存在的商品
    product_id: 999999
    expected_success: true    # 不报错(MallLite 的行为)

【新建文件】 api/test_data/order_cases.yaml

yaml 复制代码
# 订单测试数据

create_cases:
  - case_name: 正常下单
    add_product_id: 1
    add_quantity: 1
    expected_success: true

  - case_name: 空购物车下单
    add_product_id: null      # 不加购物车
    add_quantity: null
    expected_success: false   # 应该失败:购物车为空
    expected_msg: 购物车为空

query_cases:
  - case_name: 用订单号查详情
    source: create_order      # 先创建一个订单再查
    expected_has_fields:
      - order_no
      - status
      - items

1.4 更新 data_reader.py

【追加到】 api/common/data_reader.py,在原有代码末尾追加:

python 复制代码
# ===== 以下是 YAML 支持(如果之前没加的话) =====

def read_yaml(filename):
    """
    读取 YAML 数据文件

    参数:
        filename: 文件名(在 test_data 目录下)

    返回:
        解析后的 Python 对象(字典或列表)
    """
    import yaml
    filepath = DATA_DIR / filename
    if not filepath.exists():
        raise FileNotFoundError(f"数据文件不存在:{filepath}")
    with open(filepath, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    logger.debug(f"读取 YAML 数据:{filename}")
    return data

1.5 YAML 数据驱动用例

【新建文件】 api/test_cases/test_yaml_drive.py

python 复制代码
"""
YAML 数据驱动用例

对比 JSON 数据驱动(第 10 篇的 test_user.py、test_product.py):
  JSON:机器友好,不能写注释,适合 API 返回值
  YAML:人友好,能写注释,适合测试数据

知识点:什么时候用 YAML,什么时候用 JSON?
  测试数据 → YAML(需要注释说明每组数据的目的)
  API 返回值 → JSON(机器生成,不需要注释)
  配置文件 → YAML(需要注释,如 pytest.ini 的 YAML 版本)
"""

import pytest
from common.data_reader import read_yaml
from common.assertions import ApiAssertions

cart_data = read_yaml("cart_cases.yaml")
order_data = read_yaml("order_cases.yaml")


class TestCartYAML:
    """购物车 YAML 数据驱动"""

    @pytest.mark.regression
    @pytest.mark.cart
    @pytest.mark.parametrize("case", cart_data["add_cases"],
                             ids=[c["case_name"] for c in cart_data["add_cases"]])
    def test_add_to_cart(self, admin_cart_api, assert_api, case):
        """
        YAML 驱动:添加购物车

        数据来自 test_data/cart_cases.yaml 的 add_cases
        新增数据只需改 YAML 文件,不改 Python 代码
        """
        resp = admin_cart_api.add_to_cart(
            product_id=case["product_id"],
            quantity=case["quantity"],
        )

        if case["expected_success"]:
            assert_api(resp).assert_success()
        else:
            assert_api(resp).assert_biz_fail()


class TestOrderYAML:
    """订单 YAML 数据驱动"""

    @pytest.mark.regression
    @pytest.mark.order
    @pytest.mark.parametrize("case", order_data["create_cases"],
                             ids=[c["case_name"] for c in order_data["create_cases"]])
    def test_create_order(self, admin_cart_api, admin_order_api, assert_api, case):
        """
        YAML 驱动:创建订单

        数据中的 add_product_id 为 null 表示不加购物车(测试空购物车下单)
        """
        # 如果指定了商品,先加购物车
        if case["add_product_id"]:
            admin_cart_api.add_to_cart(
                product_id=case["add_product_id"],
                quantity=case["add_quantity"],
            )

        resp = admin_order_api.create_order()

        if case["expected_success"]:
            assert_api(resp).assert_success()
        else:
            assert_api(resp).assert_biz_fail()
            if case.get("expected_msg"):
                assert_api(resp).assert_message(case["expected_msg"])

二、Excel 数据驱动

2.1 为什么用 Excel

YAML 是给会写代码的测试工程师用的。但很多时候测试数据需要由产品经理、业务测试人员来维护,他们不会写 YAML,但会用 Excel。

复制代码
维护者          适合的格式
───────────    ────────────
开发/自动化工程师  YAML / JSON(注释 + 版本控制)
产品/业务测试     Excel(直观 + 不需要学格式)

2.2 安装 openpyxl

powershell 复制代码
pip install openpyxl

2.3 创建 Excel 数据文件

【新建文件】 api/test_data/login_cases.xlsx

用 Excel 软件手动创建,内容如下(或者用下面的 Python 脚本生成):

python 复制代码
# 这是一个辅助脚本,用来生成 Excel 数据文件
# 运行一次后就不用了,Excel 文件已经生成好了
# 运行方式:python api/tools/create_excel.py

import os

# 确保目录存在
os.makedirs("../test_data", exist_ok=True)

from openpyxl import Workbook

wb = Workbook()

# Sheet 1:登录成功
ws1 = wb.active
ws1.title = "登录成功"
ws1.append(["case_name", "username", "password", "expected_role"])
ws1.append(["管理员", "admin", "admin123", "admin"])
ws1.append(["普通用户", "testuser", "test123", "user"])
ws1.append(["VIP用户", "vipuser", "vip123", "vip"])

# Sheet 2:登录失败
ws2 = wb.create_sheet("登录失败")
ws2.append(["case_name", "username", "password", "expected_msg"])
ws2.append(["密码错误", "admin", "wrong", "密码错误"])
ws2.append(["用户不存在", "nobody", "123", "用户不存在"])
ws2.append(["用户名为空", "", "admin123", "请输入用户名"])
ws2.append(["密码为空", "admin", "", "请输入密码"])

wb.save("../test_data/login_cases.xlsx")
print("Excel 文件已生成:test_data/login_cases.xlsx")

生成后的 Excel 文件有两个 Sheet:

复制代码
Sheet "登录成功":
| case_name | username | password | expected_role |
|-----------|----------|----------|---------------|
| 管理员     | admin    | admin123 | admin         |
| 普通用户   | testuser | test123  | user          |
| VIP用户    | vipuser  | vip123   | vip           |

Sheet "登录失败":
| case_name | username | password | expected_msg |
|-----------|----------|----------|--------------|
| 密码错误   | admin    | wrong    | 密码错误      |
| 用户不存在 | nobody   | 123      | 用户不存在    |
| 用户名为空 |          | admin123 | 请输入用户名  |
| 密码为空   | admin    |          | 请输入密码    |

2.4 Excel 读取工具

【追加到】 api/common/data_reader.py,在文件末尾追加:

python 复制代码
def read_excel(filename, sheet_name=None):
    """
    读取 Excel 测试数据

    参数:
        filename: Excel 文件名(在 test_data 目录下)
        sheet_name: Sheet 名称(None 读第一个 Sheet)

    返回:
        列表,每个元素是一个字典(第一行是表头)
        [
            {"case_name": "管理员", "username": "admin", ...},
            {"case_name": "普通用户", "username": "testuser", ...},
        ]

    知识点:为什么返回字典列表而不是二维列表?
        字典列表可以直接用 case["username"] 取值,比 case[1] 更直观。
        而且调换 Excel 列的顺序不会影响代码。
    """
    from openpyxl import load_workbook

    filepath = DATA_DIR / filename
    if not filepath.exists():
        raise FileNotFoundError(f"Excel 文件不存在:{filepath}")

    wb = load_workbook(filepath, read_only=True)

    if sheet_name:
        ws = wb[sheet_name]
    else:
        ws = wb.active

    # 第一行是表头
    rows = list(ws.iter_rows(values_only=True))
    if len(rows) < 2:
        return []

    headers = [str(h) if h else f"col_{i}" for i, h in enumerate(rows[0])]

    # 从第二行开始是数据
    result = []
    for row in rows[1:]:
        # 跳过全空的行
        if all(cell is None for cell in row):
            continue
        item = dict(zip(headers, row))
        result.append(item)

    wb.close()
    logger.debug(f"读取 Excel:{filename}(Sheet: {sheet_name or 'active'}),共 {len(result)} 条")
    return result

2.5 Excel 数据驱动用例

【新建文件】 api/test_cases/test_excel_drive.py

python 复制代码
"""
Excel 数据驱动用例

核心价值:非技术人员(产品、业务测试)可以直接编辑 Excel 来修改测试数据,
不需要懂 Python、不需要懂 YAML 格式。

使用方式:
  1. 产品同学打开 test_data/login_cases.xlsx
  2. 在 Sheet "登录失败" 中新增一行数据
  3. 保存 Excel
  4. 运行 pytest,自动多一条用例

知识点:Excel vs YAML vs JSON 对比
  JSON:机器友好,不能写注释,Git 友好
  YAML:人友好,能写注释,Git 友好
  Excel:非技术人员友好,不能版本控制(二进制文件),不适合 CI/CD

  建议:CI/CD 用 YAML,线下跟业务同学协作用 Excel
"""

import pytest
from common.data_reader import read_excel
from common.assertions import ApiAssertions

# 读取 Excel 中两个 Sheet 的数据
login_success = read_excel("login_cases.xlsx", "登录成功")
login_fail = read_excel("login_cases.xlsx", "登录失败")


class TestLoginExcel:
    """Excel 数据驱动:登录接口"""

    @pytest.mark.regression
    @pytest.mark.login
    @pytest.mark.parametrize("case", login_success,
                             ids=[c["case_name"] for c in login_success])
    def test_login_success(self, user_api, assert_api, case):
        """
        Excel 驱动:多种用户类型登录成功

        数据来自 test_data/login_cases.xlsx 的 "登录成功" Sheet
        """
        resp = user_api.login(case["username"], case["password"])

        a = assert_api(resp)
        a.assert_success()
        a.assert_field_value("data.username", case["username"])
        a.assert_field_value("data.role", case["expected_role"])

    @pytest.mark.regression
    @pytest.mark.login
    @pytest.mark.negative
    @pytest.mark.parametrize("case", login_fail,
                             ids=[c["case_name"] for c in login_fail])
    def test_login_fail(self, user_api, assert_api, case):
        """
        Excel 驱动:各种登录失败场景

        数据来自 test_data/login_cases.xlsx 的 "登录失败" Sheet
        """
        resp = user_api.login(case["username"], case["password"])

        a = assert_api(resp)
        a.assert_biz_fail()
        a.assert_code(401)
        a.assert_message(case["expected_msg"])

2.6 三种数据驱动方式对比

维度 JSON YAML Excel
可读性 一般 最好
注释 不支持 支持 不需要(有表头)
Git 版本控制 支持 支持 不支持(二进制)
维护者 开发/测试工程师 开发/测试工程师 任何人
CI/CD 适合 适合 需要提交到仓库
嵌套数据 天然支持 天然支持 不方便(需要多 Sheet)
适用场景 API 返回值 测试数据 业务人员协作

三、Schema 自动校验进阶

3.1 第 10 篇的问题

第 10 篇的 Schema 校验需要手动调用 assert_schema("login"),用例里要写明 Schema 名称。问题:

  • 容易忘记写
  • 新增接口时要手动新增 Schema 定义

3.2 统一校验装饰器

用装饰器让 Schema 校验自动执行,不用在每条用例里手动写。

【追加到】 api/common/schemas.py,在文件末尾追加:

python 复制代码
def auto_validate(schema_name):
    """
    自动 Schema 校验装饰器

    用法:
        @auto_validate("login")
        def test_login(self, user_api):
            resp = user_api.login("admin", "admin123")
            return resp  # 装饰器会自动校验响应的 Schema

    知识点:装饰器是什么?
        装饰器是一个函数,它"包裹"另一个函数,在原函数执行前后做额外的事情。
        这里的 auto_validate 在测试函数执行后,自动对返回的 response 做 Schema 校验。
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            # 如果函数返回了一个 requests.Response 对象,自动校验
            import requests
            if isinstance(result, requests.Response):
                validate_schema(result.json(), schema_name)
            return result
        wrapper.__name__ = func.__name__
        wrapper.__doc__ = func.__doc__
        return wrapper
    return decorator

3.3 从响应自动生成 Schema

手写 Schema 容易漏字段。这个工具能从实际响应中自动生成 Schema 骨架,你再做微调。

【追加到】 api/common/schemas.py,在文件末尾追加:

python 复制代码
def generate_schema(data, max_depth=3):
    """
    从实际响应数据自动生成 JSON Schema 骨架

    参数:
        data: 实际的 JSON 数据(通常是 resp.json())
        max_depth: 最大递归深度(避免无限嵌套)

    返回:
        JSON Schema 字典

    用法:
        resp = user_api.login("admin", "admin123")
        schema = generate_schema(resp.json())
        print(json.dumps(schema, indent=2, ensure_ascii=False))
        # 输出可以作为 SCHEMAS 字典中的定义

    知识点:为什么要自动生成?
        手写 Schema 需要逐个字段写 type、required。
        如果接口有 50 个字段,手写容易漏。
        自动生成一个骨架,再人工微调,效率高很多。
    """
    if max_depth <= 0:
        return {}

    if isinstance(data, dict):
        schema = {"type": "object", "required": list(data.keys()), "properties": {}}
        for key, value in data.items():
            schema["properties"][key] = generate_schema(value, max_depth - 1)
        return schema

    elif isinstance(data, list):
        schema = {"type": "array"}
        if data:
            schema["items"] = generate_schema(data[0], max_depth - 1)
        return schema

    elif isinstance(data, bool):
        return {"type": "boolean"}

    elif isinstance(data, int):
        return {"type": "integer"}

    elif isinstance(data, float):
        return {"type": "number"}

    elif isinstance(data, str):
        return {"type": "string"}

    elif data is None:
        return {"type": "null"}

    return {}

3.4 实战:自动 Schema 校验用例

【新建文件】 api/test_cases/test_schema_auto.py

python 复制代码
"""
Schema 自动校验实战

两种方式:
  1. assert_schema() 手动校验(第 10 篇已用)
  2. generate_schema() 自动生成 Schema 骨架
"""

import pytest
import json
from api.common.schemas import validate_schema, generate_schema
from api.common.assertions import ApiAssertions


class TestSchemaManual:
    """手动 Schema 校验(回顾)"""

    @pytest.mark.regression
    @pytest.mark.schema
    def test_login_schema(self, user_api, assert_api, admin_account):
        """
        手动校验:登录响应符合 Schema

        知识点:为什么每个接口都要校验 Schema?
          Schema 校验不是检查"数据对不对",而是检查"格式对不对"。
          如果接口返回了 {"code": 200} 但没有 "data" 字段,
          后面的 get_field("data.username") 会直接报错。
          Schema 校验能提前发现这种结构变更。
        """
        resp = user_api.login(admin_account["username"], admin_account["password"])
        assert_api(resp).assert_success().assert_schema("login")

    @pytest.mark.regression
    @pytest.mark.schema
    def test_product_list_schema(self, product_api, assert_api):
        """手动校验:商品列表"""
        resp = product_api.get_products()
        assert_api(resp).assert_success().assert_schema("product_list")

    @pytest.mark.regression
    @pytest.mark.schema
    def test_cart_schema(self, admin_cart_api, assert_api):
        """手动校验:购物车"""
        admin_cart_api.add_to_cart(product_id=1, quantity=1)
        resp = admin_cart_api.get_cart()
        assert_api(resp).assert_success().assert_schema("cart")

    @pytest.mark.regression
    @pytest.mark.schema
    def test_category_schema(self, product_api, assert_api):
        """手动校验:分类列表"""
        resp = product_api.get_categories()
        assert_api(resp).assert_success().assert_schema("category_list")


class TestSchemaGenerate:
    """
    自动生成 Schema 骨架

    知识点:自动生成的工作流程
      1. 用 generate_schema(resp.json()) 生成骨架
      2. 检查生成的 Schema 是否合理
      3. 微调后复制到 schemas.py 中
      4. 后续用 assert_schema() 做回归校验
    """

    @pytest.mark.regression
    @pytest.mark.schema
    def test_generate_login_schema(self, user_api, admin_account):
        """生成登录响应的 Schema 骨架"""
        resp = user_api.login(admin_account["username"], admin_account["password"])
        body = resp.json()

        schema = generate_schema(body)

        # 验证骨架的基本结构
        assert schema["type"] == "object"
        assert "code" in schema["properties"]
        assert "message" in schema["properties"]
        assert "data" in schema["properties"]

        # 打印生成的 Schema(运行时可以看到)
        print(f"\n生成的登录 Schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}")

    @pytest.mark.regression
    @pytest.mark.schema
    def test_generate_product_list_schema(self, product_api):
        """生成商品列表的 Schema 骨架"""
        resp = product_api.get_products(page_size=1)
        body = resp.json()

        schema = generate_schema(body)

        assert schema["type"] == "object"
        assert "code" in schema["properties"]
        assert "data" in schema["properties"]

        print(f"\n生成的商品列表 Schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}")

四、Allure 报告集成

4.1 什么是 Allure

pytest 默认的报告是终端输出的文本。Allure 是一个可视化测试报告框架,能生成带图表、截图、日志的 HTML 报告。

复制代码
pytest 默认报告:                  Allure 报告:
  ==================              ┌────────────────────────┐
  73 passed, 4 failed             │ 📊 用例总数: 77         │
  ==================              │ ✅ 通过: 73  ❌ 失败: 4  │
                                  │ 📈 通过率: 94.8%        │
                                  │ ⏱ 耗时: 45s             │
                                  ├────────────────────────┤
                                  │ 按模块分组               │
                                  │ 按严重级别分组           │
                                  │ 每条用例的请求/响应日志   │
                                  └────────────────────────┘

4.2 前置条件

第 10 篇的 requirements.txt 已经加了 allure-pytest。确认已安装:

powershell 复制代码
pip install allure-pytest

还需要安装 Allure 命令行工具(用来生成 HTML 报告):

powershell 复制代码
# 方式 1:用 scoop 安装(推荐)
scoop install allure

# 方式 2:手动下载
# 去 https://github.com/allure-framework/allure2/releases 下载
# 解压后把 bin 目录加到 PATH 环境变量

4.3 conftest.py 添加 Allure Hook

【追加到】 api/conftest.py,在文件末尾追加:

python 复制代码
import allure

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    Allure Hook:自动附加请求/响应日志到报告

    知识点:hookwrapper 是什么?
      Pytest 的 hook 机制允许你在测试执行的各个阶段插入自定义逻辑。
      hookwrapper 会在用例执行完成后触发。
      我们在这里捕获失败的用例,把请求/响应信息附加到 Allure 报告中。
    """
    outcome = yield
    report = outcome.get_result()

    if report.when == "call" and report.failed:
        # 用例执行失败时,附加额外信息到报告
        # 这些信息会在 Allure 报告的"附件"区域显示
        try:
            allure.attach(
                name="测试失败信息",
                body=str(report.longrepr),
                attachment_type=allure.attachment_type.TEXT,
            )
        except Exception:
            pass

4.4 用例中使用 Allure 功能

Allure 提供三个核心功能:分类 (严重级别、模块)、Step (步骤描述)、附件

【追加到】 api/test_cases/test_allure_demo.py(新建文件):

python 复制代码
"""
Allure 报告演示

这个文件演示 Allure 的核心功能:
  1. severity - 严重级别分类
  2. feature/story - 功能/场景分类
  3. step - 步骤描述
  4. attach - 附加信息

知识点:Allure 报告中的层级关系
  feature(功能模块)
    └── story(用户场景)
          └── test_case(测试用例)
                └── step(步骤)

  在 MallLite 中:
    feature = "商品管理"
    story = "商品搜索"
    test_case = "按关键词搜索"
    step = "发送搜索请求"、"验证返回结果"
"""

import allure
import pytest
from api.common.assertions import ApiAssertions
from api.common.extractor import Extractor


@allure.feature("商品管理")
class TestProductAllure:
    """商品接口 - Allure 报告演示"""

    @allure.story("商品列表")
    @allure.severity(allure.severity_level.BLOCKER)
    @pytest.mark.smoke
    @pytest.mark.product
    def test_product_list(self, product_api, assert_api):
        """
        获取商品列表

        severity = BLOCKER(阻塞级)
        这意味着如果这条用例失败,其他所有商品相关的用例都不用跑了

        知识点:Allure 严重级别
          BLOCKER   - 阻塞级:失败后其他用例无法执行
          CRITICAL  - 严重级:核心功能失败
          NORMAL    - 普通级:常规功能
          MINOR     - 次要级:非核心功能
          TRIVIAL   - 无关紧要:锦上添花的功能
        """
        with allure.step("发送获取商品列表请求"):
            resp = product_api.get_products()
            body = assert_api(resp).assert_success().body

        with allure.step("验证返回数据结构"):
            items = Extractor.get(body, "data.items")
            assert len(items) > 0, "商品列表不应为空"

        with allure.step("附加响应数据到报告"):
            allure.attach(
                name="商品列表响应",
                body=str(body)[:500],
                attachment_type=allure.attachment_type.TEXT,
            )

    @allure.story("商品搜索")
    @allure.severity(allure.severity_level.CRITICAL)
    @pytest.mark.regression
    @pytest.mark.product
    @pytest.mark.parametrize("keyword", ["iPhone", "华为", "AirPods"],
                             ids=["iPhone", "华为", "AirPods"])
    def test_product_search(self, product_api, keyword):
        """
        数据驱动搜索

        severity = CRITICAL(严重级)
        搜索是核心功能,但不影响其他模块
        """
        with allure.step(f"搜索关键词:{keyword}"):
            resp = product_api.get_products(keyword=keyword)
            body = resp.json()

        with allure.step("验证搜索成功"):
            assert body["code"] == 200

        with allure.step("验证有搜索结果"):
            items = body["data"]["items"]
            assert len(items) > 0, f"搜索 '{keyword}' 无结果"

    @allure.story("商品详情")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.regression
    @pytest.mark.product
    def test_product_detail(self, product_api, assert_api):
        """
        查看商品详情

        severity = NORMAL(普通级)
        """
        with allure.step("获取商品列表"):
            resp = product_api.get_products(page_size=1)
            product_id = assert_api(resp).get_field("data.items")[0]["id"]

        with allure.step(f"查看商品 {product_id} 的详情"):
            resp = product_api.get_product(product_id)
            a = assert_api(resp)
            a.assert_success()

        with allure.step("验证详情数据完整"):
            a.assert_has_field("data.name")
            a.assert_has_field("data.price")
            a.assert_has_field("data.stock")


@allure.feature("购物车")
class TestCartAllure:
    """购物车接口 - Allure 报告演示"""

    @allure.story("添加商品")
    @allure.severity(allure.severity_level.CRITICAL)
    @pytest.mark.smoke
    @pytest.mark.cart
    def test_add_to_cart(self, admin_cart_api, assert_api):
        """
        添加商品到购物车

        用 step 装饰器把操作分成清晰的步骤
        """
        with allure.step("添加商品 ID=1 到购物车"):
            resp = admin_cart_api.add_to_cart(product_id=1, quantity=2)
            assert_api(resp).assert_success()

        with allure.step("验证购物车中有该商品"):
            resp = admin_cart_api.get_cart()
            items = assert_api(resp).get_field("data.items")
            product_ids = [i["product_id"] for i in items]
            assert 1 in product_ids

        with allure.step("验证数量正确"):
            item = next(i for i in items if i["product_id"] == 1)
            assert item["quantity"] == 2


@allure.feature("订单")
class TestOrderAllure:
    """订单接口 - Allure 报告演示"""

    @allure.story("创建订单")
    @allure.severity(allure.severity_level.BLOCKER)
    @pytest.mark.smoke
    @pytest.mark.order
    def test_create_order(self, admin_cart_api, admin_order_api, assert_api):
        """
        创建订单

        severity = BLOCKER:下单是购物流程的最后一步,失败意味着用户无法完成购买
        """
        with allure.step("清空购物车并添加商品"):
            admin_cart_api.clear_cart()
            admin_cart_api.add_to_cart(product_id=1, quantity=1)

        with allure.step("创建订单"):
            resp = admin_order_api.create_order()
            a = assert_api(resp)
            a.assert_success()

        with allure.step("验证订单号格式"):
            order_no = a.get_field("data.order_no")
            assert "ORD" in order_no
            allure.attach(name="订单号", body=order_no,
                          attachment_type=allure.attachment_type.TEXT)

4.5 生成和查看报告

powershell 复制代码
# 第一步:运行测试,生成 Allure 原始数据
pytest --alluredir=allure-results

# 第二步:用 Allure 生成 HTML 报告
allure serve allure-results

# 或者生成到文件
allure generate allure-results -o allure-report --clean

Allure 报告的效果:

复制代码
┌─────────────────────────────────────────┐
│ 📊 总览                                 │
│ 通过: 73  失败: 4  通过率: 94.8%        │
├─────────────────────────────────────────┤
│ 按功能分组                              │
│   商品管理 (12)                         │
│     ├── 商品列表 (3)                    │
│     ├── 商品搜索 (4)                    │
│     └── 商品详情 (5)                    │
│   购物车 (9)                            │
│   订单 (8)                              │
├─────────────────────────────────────────┤
│ 按严重级别                              │
│   🔴 BLOCKER: 3                         │
│   🟠 CRITICAL: 8                        │
│   🟡 NORMAL: 45                         │
│   🟢 MINOR: 17                          │
├─────────────────────────────────────────┤
│ 每条用例可展开查看                      │
│   ├── 步骤详情                          │
│   ├── 请求参数                          │
│   ├── 响应数据                          │
│   └── 失败截图/日志                     │
└─────────────────────────────────────────┘

4.6 更新 pytest.ini

【修改】 api/pytest.ini,在 addopts 中追加 Allure 数据目录:

ini 复制代码
[pytest]
testpaths = test_cases
addopts = -v --tb=short --strict-markers --alluredir=allure-results

markers =
    smoke: 冒烟测试
    regression: 回归测试
    login: 登录接口
    register: 注册接口
    product: 商品接口
    category: 分类接口
    cart: 购物车接口
    order: 订单接口
    positive: 正向用例
    negative: 反向用例
    schema: Schema 校验用例
    p0: 最高优先级
    p1: 高优先级
    p2: 中优先级

log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s | %(levelname)-8s | %(name)-12s | %(message)s
log_cli_date_format = %H:%M:%S

只改了 addopts,追加了 --alluredir=allure-results。这样每次运行 pytest 都会自动生成 Allure 原始数据。


五、运行验证

5.1 运行全部用例

powershell 复制代码
pytest -v

5.2 按标记运行

powershell 复制代码
# 只跑 Schema 校验
pytest -v -m schema

# 只跑 YAML 驱动
pytest test_cases/test_yaml_drive.py -v

# 只跑 Excel 驱动
pytest test_cases/test_excel_drive.py -v

# 只跑 Allure 演示
pytest test_cases/test_allure_demo.py -v

5.3 生成 Allure 报告

powershell 复制代码
# 运行并生成 Allure 数据
pytest --alluredir=allure-results

# 打开报告
allure serve allure-results

六、本篇新增和修改的文件

文件 操作 说明
test_data/cart_cases.yaml 新建 购物车 YAML 测试数据
test_data/order_cases.yaml 新建 订单 YAML 测试数据
test_data/login_cases.xlsx 新建 登录 Excel 测试数据
common/data_reader.py 追加 read_yaml + read_excel 方法
common/schemas.py 追加 auto_validate 装饰器 + generate_schema
test_cases/test_yaml_drive.py 新建 YAML 数据驱动用例
test_cases/test_excel_drive.py 新建 Excel 数据驱动用例
test_cases/test_schema_auto.py 新建 Schema 自动校验用例
test_cases/test_allure_demo.py 新建 Allure 报告演示用例
conftest.py 追加 Allure hook(自动附加失败信息)
pytest.ini 修改 addopts 追加 --alluredir

七、接口自动化篇总结

7.1 完整项目结构

复制代码
api/
├── api_objects/                    ← API 封装层
│   ├── base_api.py                 ← ApiClient 基类(Session + 重试 + 日志)
│   ├── user_api.py                 ← 用户接口(登录、注册)
│   ├── product_api.py              ← 商品接口(列表、详情、搜索)
│   ├── cart_api.py                 ← 购物车接口(增删改查)
│   └── order_api.py                ← 订单接口(创建、列表、详情)
│
├── common/                         ← 工具层
│   ├── logger.py                   ← 日志
│   ├── assertions.py               ← 断言工具(success/biz_fail/schema/time)
│   ├── response_validator.py       ← 统一响应校验
│   ├── schemas.py                  ← JSON Schema 定义 + 自动生成
│   ├── extractor.py                ← 响应数据提取器
│   └── data_reader.py              ← 测试数据读取(JSON/YAML/Excel)
│
├── test_cases/                     ← 测试用例
│   ├── test_user.py                ← 登录/注册(JSON 数据驱动)
│   ├── test_product.py             ← 商品(JSON 数据驱动)
│   ├── test_cart.py                ← 购物车 CRUD
│   ├── test_order.py               ← 订单 CRUD
│   ├── test_shopping_flow.py       ← 链路测试 + fixture 链式依赖
│   ├── test_extractor_practice.py  ← Extractor 实战
│   ├── test_data_consistency.py    ← 跨接口数据一致性
│   ├── test_session.py             ← Session 验证专题
│   ├── test_pagination.py          ← 分页测试
│   ├── test_edge_cases.py          ← 边界值和异常场景
│   ├── test_auth.py                ← 鉴权验证
│   ├── test_resilience.py          ← 超时/重试/并发
│   ├── test_yaml_drive.py          ← YAML 数据驱动
│   ├── test_excel_drive.py         ← Excel 数据驱动
│   ├── test_schema_auto.py         ← Schema 自动校验
│   └── test_allure_demo.py         ← Allure 报告演示
│
├── test_data/                      ← 测试数据
│   ├── login_cases.json            ← 登录 JSON 数据
│   ├── product_cases.json          ← 商品 JSON 数据
│   ├── cart_cases.yaml             ← 购物车 YAML 数据
│   ├── order_cases.yaml            ← 订单 YAML 数据
│   └── login_cases.xlsx            ← 登录 Excel 数据
│
├── examples/                       ← 独立示例(不在 MallLite 上运行)
│   ├── mock_demo.py                ← Mock 外部接口示例
│   └── sign_demo.py                ← 接口签名示例
│
├── conftest.py                     ← fixture + Allure hook
├── pytest.ini                      ← 运行配置
└── requirements.txt                ← 依赖

7.2 最佳实践

实践 说明
接口封装分层 base_api → 业务 api → conftest fixture → 测试用例,四层分离
Session 共享 登录一次,session 级共享,避免重复登录
数据清理 autouse fixture 自动清理,避免用例间污染
断言层次 能通 → 结构 → 业务码 → 字段存在 → 字段值 → 数据逻辑 → 响应时间
数据驱动 需要注释用 YAML,需要非技术人员维护用 Excel
边界值思维 五个维度:空值、范围、不存在、恶意输入、状态冲突
链路测试 用变量传递数据,所有 API 共享 session
fixture 链式依赖 需要前置数据时用 fixture,不需要时直接用变量
Schema 校验 核心接口必须校验,新接口用 generate_schema 生成骨架
Allure 报告 核心用例标 severity,步骤用 step 装饰器

7.3 常见坑

现象 解法
Session 没共享 加购物车返回"请先登录" 所有 API 实例共享同一个 session
用例间数据污染 用例 B 的购物车有上一条残留 autouse fixture 每次清空
购物车不隔离 用户 A 加的商品出现在用户 B MallLite 的设计,用独立清空缓解
HTTP 200 ≠ 成功 401 用户不存在也是 HTTP 200 用 code 判断,不用 HTTP status
购物车用 product_id cart_item_id 找不到 MallLite 的增删改查都用 product_id
订单没有 id 字段 用 id 查详情报 KeyError 用 order_no 查详情
注册不写库 注册后登录返回用户不存在 用已有账号测试链路
分页 page_size=0 HTTP 500 服务端崩溃 标记为已知 bug(xfail)

7.4 数据统计

指标 数值
用例总数 ~110+
执行时间 ~45 秒
覆盖接口 全部 4 个模块(用户、商品、购物车、订单)
测试类型 单接口、链路、数据一致性、分页、边界值、Session、鉴权、并发
数据驱动格式 JSON、YAML、Excel 三种

八、下篇预告

14 - App 自动化基础与环境

Web 自动化和接口自动化都完成了。下一篇进入 App 自动化领域:Appium 环境搭建、ADB 常用命令、元素定位方式、用 MallLite 的移动端 Web 版做第一个 App 测试,同时讲清楚移动 Web 测试和原生 App 测试的核心差异。


接口自动化篇完成。下一篇:App 自动化基础与环境(14/18)。

相关推荐
程序员无隅2 小时前
Codex 实战:用 AI 写运维脚本
运维·人工智能
Soari2 小时前
Linux 内核对比:PREEMPT_DYNAMIC vs PREEMPT_RT
linux·运维·ubuntu
商业看点解说2 小时前
数据中心运维管理软件平台推荐
运维
网硕互联的小客服2 小时前
服务器可以远程连接,但是为什么无法Ping通?如何解决处理?
运维·服务器
暖核2 小时前
Docker 入门|吃透容器生态与底层原理
运维·docker·容器
滕州市燕猫虎计算机科技工作室个体工商户2 小时前
Docker常用命令
运维·docker·容器
海宇数据2 小时前
零信任架构实战:基于海宇单人婚姻状态查询构建自动化KYC审核网关
运维·人工智能·架构·自动化
Lydia 不加班3 小时前
Windows 服务器 人大金仓 54321 端口放行完整步骤
运维·服务器·windows
运维全栈笔记3 小时前
Windows 本地部署 Codex 全攻略:CC Switch 接入 DeepSeek 与模型自由切换
windows·深度学习·机器学习·chatgpt