前言
第 10 篇用了 JSON 文件做数据驱动,Schema 校验也只在几条用例里手动调用。本篇把这两件事系统化,同时接入 Allure 报告,让接口自动化的产出能被团队其他人看懂。
本篇五件事:
- YAML 数据驱动:把 JSON 测试数据迁移到 YAML,对比优劣
- Excel 数据驱动:用 openpyxl 读 Excel,适合非技术人员维护数据
- Schema 自动校验进阶:conftest 中统一校验 + 从响应自动生成 Schema
- Allure 报告集成:请求/响应自动附加、Step 装饰器、用例分类、严重级别
- 接口自动化篇总结:完整项目结构、最佳实践、常见坑
一、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)。