Chapter 13 · Data-Driven and Parameterized E2E
Chapter goals: Use parametrize, fixture params, and external data sources to turn "one flow × many data sets" into structured E2E cases, and learn to control parameter scale.
13.1 Why E2E Needs Data-Driven Testing Too
A login form's validation logic has a dozen input combinations (empty password, malformed email, valid credentials…); writing a dozen nearly identical test functions is a maintenance nightmare. The core idea of data-driven testing: separate "the operation flow" from "the test data" — write the flow once, list the data as a table, let the framework generate cases in bulk.
In E2E there are three common parameterization paths:
@pytest.mark.parametrize: same flow, multiple data sets;- Fixture params: entire environment/role dimension differs (e.g., regular user vs admin);
- External data files (YAML/JSON): large data sets or non-programmer-maintained data.
13.2 parametrize for Form Validation
Using a registration form as an example:
# tests/test_signup_validation.py
import re
import pytest
from playwright.sync_api import Page, expect
# (email, password, expected message) triplets covering equivalence classes
INVALID_CASES = [
("", "Passw0rd!", "Please enter email"),
("not-an-email", "Passw0rd!", "Email format is invalid"),
("user@test.com", "", "Please enter password"),
("user@test.com", "123", "Password must be at least 8 characters"),
]
@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("Email").fill(email)
page.get_by_label("Password").fill(password)
page.get_by_role("button", name="Sign up").click()
# Web-First assertion auto-waits for the message to appear
expect(page.get_by_role("alert")).to_contain_text(message)One run produces 4 independent cases; any failure pinpoints exactly which data set caused it — something impossible when cramming all cases into one function.
Readable Report IDs
Default case names拼 the parameter values, which look ugly for long strings. Use ids to customize naming:
@pytest.mark.parametrize(
"email,password,message",
INVALID_CASES,
ids=["empty_email", "bad_format", "empty_password", "too_short"],
)
def test_signup_validation(page: Page, email, password, message):
...You can also pass a function for dynamic generation: ids=lambda v: f"case-{v}" (called per parameter). Combined with -k "empty_email" you can filter runs by name.
13.3 Fixture Params for Multi-Role Scenarios
parametrize parameterizes "one data set"; when the difference is in the entire environment identity (different role accounts, different browser context config), fixture params are more appropriate:
# 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=["admin", "buyer"])
def logged_in_page(request, page: Page) -> Page:
"""Each role gets its own logged-in independent page"""
cred = ROLES[request.param]
page.goto("/login")
page.get_by_label("Email").fill(cred["user"])
page.get_by_label("Password").fill(cred["passwd"])
page.get_by_role("button", name="Log in").click()
expect(page.get_by_role("navigation")).to_contain_text(cred["user"])
yield page
page.context.close() # Ensure session isolation# tests/test_dashboard.py
def test_dashboard_visible(logged_in_page):
"""This test runs once for admin and once for buyer"""
expect(logged_in_page.locator("#dashboard")).to_be_visible()Each value in params instantiates the fixture once; tests depending on it double — extending the role dimension requires zero changes to test code.
13.4 External Data Sources: YAML for Complex Data
When data exceeds a dozen rows, putting it in a Python file clutters readability; move it into YAML loaded by conftest.py:
# conftest.py
import pytest
import yaml
with open("tests/data/signup_cases.yaml") as f:
SIGNUP_CASES = yaml.safe_load(f)
@pytest.mark.parametrize(
"email,password,message",
SIGNUP_CASES,
ids=[c["email"] or "empty" for c in SIGNUP_CASES],
)
def test_signup(page, email, password, message):
...# tests/data/signup_cases.yaml
- {email: "", password: "Passw0rd!", message: "Please enter email"}
- {email: "bad@", password: "Passw0rd!", message: "Invalid email format"}
- {email: "a@b.com", password: "", message: "Please enter password"}Separating data from logic makes non-programmers (PMs, QAs) able to maintain test data independently — the most common handoff point in production teams.
13.5 Controlling Parameter Scale
A cartesian product of 10 × 10 × 10 = 1000 cases will grind CI to a halt. Rules of thumb:
- Principle of maximum coverage, minimum cases: pick boundary values and equivalence classes, not every permutation;
- Cap at ~50 cases per test file; split into files when exceeded;
- Use
--collect-onlyto preview the generated case count before running; - Skip slow cases in fast feedback loops with
-kfilters.
13.6 Chapter Summary
- parametrize separates flow from data; one flow, many data sets, each a standalone case;
- fixture params shine when the dimension is "environment/role", not just a value;
- YAML externalization lets non-programmers maintain test data;
- Cap case count; prefer boundary/equivalence values over exhaustive cartesian products.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What is the core benefit of data-driven E2E testing?
2. When should you use fixture params instead of @pytest.mark.parametrize?
3. What is the main risk of a 10×10×10 cartesian parametrize in E2E?
4. How do you preview the number of generated cases without running them?
🛠️ Hands-On Exercises
- Take any form with 3 fields; write 8 parametrize cases covering empty, too-short, wrong-format, and valid inputs; assert each error message.
- Refactor the login tests from Chapter 9 to use fixture params for admin vs buyer roles; observe the test count double with zero test-code changes.
- Move test data to a YAML file; add a new case without touching the test function; run and confirm the new case appears in the report.
Proceed to Chapter 14: Page Object Model.