Skip to content

第 13 章 · 数据驱动与参数化 E2E

本章目标:用 parametrize、fixture params 和外部数据源把"同一套操作 × 多份测试数据"变成结构化的 E2E 用例,并学会控制参数规模。

13.1 为什么 E2E 也需要数据驱动

一个登录表单的校验逻辑有十几种输入组合(空密码、错误格式邮箱、正确凭据……),手写十几个几乎相同的测试函数是维护灾难。数据驱动的核心思想是:把"操作流程"与"测试数据"分离——流程写一遍,数据列成表,由框架批量生成用例。

E2E 场景下有三条常用的参数化路径:

  1. @pytest.mark.parametrize:同一场流程跑多份数据;
  2. fixture params:整个环境/角色维度不同(如普通用户 vs 管理员);
  3. 外部数据文件(YAML/JSON):数据量大或需要非程序员维护时。

13.2 parametrize 驱动表单校验

以注册表单校验为例:

python
# tests/test_signup_validation.py
import re
import pytest
from playwright.sync_api import Page, expect

# (邮箱, 密码, 预期提示文案) 三元组覆盖各等价类
INVALID_CASES = [
    ("", "Passw0rd!", "请输入邮箱"),
    ("not-an-email", "Passw0rd!", "邮箱格式不正确"),
    ("user@test.com", "", "请输入密码"),
    ("user@test.com", "123", "密码至少 8 位"),
]

@pytest.mark.parametrize("email,password,message", INVALID_CASES)
def test_signup_shows_validation(page: Page, email: str, password: str, message: str):
    page.goto("/signup")
    page.get_by_label("邮箱").fill(email)
    page.get_by_label("密码").fill(password)
    page.get_by_role("button", name="注册").click()
    # Web-First 断言自动等待提示出现
    expect(page.get_by_role("alert")).to_contain_text(message)

一次运行生成 4 个独立用例,任何一个失败都能在报告中精确定位到是哪组数据——这是把所有情况塞进一个函数里做不到的。

用 ids 让报告可读

默认用例名会把参数值拼进去,长字符串会很难看。用 ids 自定义命名:

python
@pytest.mark.parametrize(
    "email,password,message",
    INVALID_CASES,
    ids=["空邮箱", "坏邮箱格式", "空密码", "密码过短"],  # 用例名直接可读
)
def test_signup_validation(page: Page, email, password, message):
    ...

也可以传函数动态生成:ids=lambda val: f"case-{val}"(对每个参数依次调用)。配合 -k "空邮箱" 还能按中文名筛选运行。

13.3 fixture params 驱动多角色场景

parametrize 参数化的是"一份数据";当差异在于整个环境身份(不同角色的账号、不同的浏览器上下文配置)时,fixture params 更合适:

python
# conftest.py
import pytest
from playwright.sync_api import Page

ROLES = {
    "admin":   {"user": "admin@shop.io",   "passwd": "Adm1n!Secret"},
    "buyer":   {"user": "buyer@shop.io",   "passwd": "Buyer!Secret"},
}

@pytest.fixture(params=list(ROLES), ids=["管理员", "买家"])
def logged_in_page(request, page: Page) -> Page:
    """每个角色都会得到一个已登录的独立页面"""
    cred = ROLES[request.param]
    page.goto("/login")
    page.get_by_label("邮箱").fill(cred["user"])
    page.get_by_label("密码").fill(cred["passwd"])
    page.get_by_role("button", name="登录").click()
    expect(page.get_by_role("navigation")).to_contain_text(cred["user"])
    yield page
    page.context.close()  # 确保会话隔离
python
# tests/test_dashboard.py
def test_dashboard_visible(logged_in_page):
    """这条测试会对 admin 和 buyer 各跑一次"""
    expect(logged_in_page.locator("#dashboard")).to_be_visible()

params 中每个值都会实例化一次该 fixture,依赖它的测试随之翻倍——角色维度的扩展不需要改任何测试代码

13.4 外部数据源:YAML 组织复杂数据

数据超过十几行后放 Python 文件里会干扰阅读,挪进 YAML 由 conftest.py 统一加载:

yaml
# data/products.yaml
- name: 机械键盘
  price: "399.00"
  stock: 有货
- name: 显示器支架
  price: "129.00"
  stock: 缺货
python
# tests/test_search.py
import pathlib
import yaml
import pytest
from playwright.sync_api import Page, expect

DATA = yaml.safe_load(
    (pathlib.Path(__file__).parent.parent / "data" / "products.yaml").read_text("utf-8")
)

@pytest.mark.parametrize("item", DATA, ids=lambda i: i["name"])  # 用商品名做 id
def test_product_card(page: Page, item: dict):
    page.goto(f"/search?q={item['name']}")
    card = page.locator(".product-card").first
    expect(card.get_by_class_name("price")).to_have_text(item["price"])
    # 库存徽标文案取决于数据而非代码
    expect(card.get_by_test_id("stock-badge")).to_contain_text(item["stock"])

JSON 同理(json.load)。要点是:数据文件只描述事实,断言逻辑保持单一;新增商品只需改 YAML,不动任何测试代码。

13.5 控制参数爆炸:等价类思维

参数化最大的陷阱是组合爆炸:4 个字段 × 每字段 5 个取值 = 625 个 E2E 用例,每个都要开浏览器。生产级做法:

  • 等价类划分:每张表单挑出边界值 + 一个典型值即可(空 / 超长 / 非法格式 / 合法),不必穷举;
  • 分层拆分:纯校验逻辑下沉到单元/API 层测(毫秒级),E2E 只保留"端到端能走通"的代表路径(第 17 章的 API 测试正是承接点);
  • 正交采样:多字段组合用 pairwise 思路人工挑选几十个代表组合,而不是全量笛卡尔积;
  • 慢路径复用状态:参数化用例共享同一个 storage_state(第 9 章)跳过重复登录,把每条用例的时间从分钟级压到秒级。

判断标准

新增一条参数前先问:它会改变被验证的行为分支吗?只是换个等价输入的,砍掉。

13.6 本章小结

  • parametrize 管"数据"、fixture params 管"环境/角色",两者互补;
  • ids 让报告里的用例名可读可筛选;
  • YAML/JSON 数据文件让非开发人员也能维护测试数据;
  • 用等价类和分层测试控制 E2E 参数总量,避免浏览器级的组合爆炸。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 同一套注册流程要用 4 组非法输入分别验证,最合适的做法是?

2. fixture 的 params=[...] 会产生什么效果?

3. E2E 表单校验的参数组合达到几百个时,正确的处理思路是?

4. 关于 parametrize 的 ids 参数,下列说法正确的是?

🛠️ 动手实践

  1. 为一个搜索框写出 5 组参数化用例(含中文、特殊字符、超长字符串、空串、正常词),用 ids 命名。
  2. 把 13.3 的 ROLES 扩展出一个"访客"角色(不登录直接访问),观察哪些测试通过哪些失败。
  3. 将本章 YAML 示例改成 JSON 实现,体会两种格式的取舍,并把数据文件移到 tests/data/ 下。

完成后进入下一章:Page Object Model 设计模式