Skip to content

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:

  1. @pytest.mark.parametrize: same flow, multiple data sets;
  2. Fixture params: entire environment/role dimension differs (e.g., regular user vs admin);
  3. 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:

python
# 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:

python
@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:

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=["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
python
# 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:

python
# 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):
    ...
yaml
# 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-only to preview the generated case count before running;
  • Skip slow cases in fast feedback loops with -k filters.

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 ​

  1. Take any form with 3 fields; write 8 parametrize cases covering empty, too-short, wrong-format, and valid inputs; assert each error message.
  2. 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.
  3. 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.