Chapter 14Lesson 04~195 minutes

Test Data, Parameterization, Fixtures, and Environment Configuration: Diagnostics, Failure Modes, and Production Practices

Many “flaky Selenium” incidents are actually data or environment incidents. This lesson makes those failures observable: shared mutable accounts, order dependence, unrecorded randomness, teardown gaps, secret leakage, hard-coded or unsafe targets, and parameter IDs that provide no diagnostic context.

DiagnosticsShared stateSecretsTarget guardCleanup

Learning objectives

  • Apply the chapter diagnostic sequence to data/configuration failures before changing waits or retries.
  • Interpret order-dependent assertions and shared-account collisions as state-ownership defects.
  • Detect skipped teardown and preserve first-failure evidence without exposing secrets.
  • Guard environment selection so a production-like or non-loopback target cannot be selected accidentally in labs.
  • Repair opaque case identities and unseeded generated inputs so failures become reproducible.

1. Diagnostic sequence: preserve before changing

  1. Preserve first-failure case ID, traceback, target name/URL, sanitized configuration, Selenium/browser versions, session ID, and relevant AUT state.
  2. Confirm the exact parameter row/seed and whether any prior test mutated shared state.
  3. Confirm fixture scope and whether teardown executed.
  4. Inspect session/context/locator/synchronization state only after the data/environment preconditions are proven.
  5. Inspect AUT/network evidence and Grid/CI capacity if remote.
  6. Apply the least destructive correction and rerun the smallest identical case.
Do not “fix” a data race with a larger wait or retry. A retry can make two tests collide less often while leaving the ownership defect intact.

2. Failure: shared mutable test account

The following example makes the Failure: shared mutable test account behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Intentionally broken design
SHARED_EMAIL = "shared@example.test"

def test_disable_account():
    api_set_enabled(SHARED_EMAIL, False)
    assert ui_login(SHARED_EMAIL) == "disabled"

def test_change_role():
    api_set_role(SHARED_EMAIL, "admin")
    assert ui_role(SHARED_EMAIL) == "admin"

Run concurrently and these scenarios race on one AUT record. The browser sessions may be perfectly isolated. Repair by assigning each case a unique deterministic synthetic identity and deleting/resetting only that identity.

3. Failure: order-dependent data

The following example makes the Failure: order-dependent data behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Intentionally broken: test_02 depends on test_01 side effects.
def test_01_create_user():
    api_create({"email": "ordered@example.test"})

def test_02_show_user():
    assert RegistrationPage(driver, base_url).find("ordered@example.test")

If test_02 runs first or alone, it fails. That is not a locator defect. Each test must create or fixture its own prerequisite inside its lifecycle.

4. Failure: unseeded random generator

The following example makes the Failure: unseeded random generator behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Broken evidence: the failing input cannot be reconstructed later.
import random
quantity = random.randint(1, 1000)
run_case(quantity)

The following example makes the Failure: unseeded random generator behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Repair: either use explicit boundary cases or record a deterministic seed.
import random
seed = 20260828
rng = random.Random(seed)
quantity = rng.randint(1, 1000)
print({"seed": seed, "quantity": quantity})
run_case(quantity)

5. Failure: teardown skipped after assertion

The following example makes the Failure: teardown skipped after assertion behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Broken: reset is never reached when the assertion fails.
driver = webdriver.Chrome()
api_reset(base_url)
page = RegistrationPage(driver, base_url)
page.open()
assert page.submit(row) == "wrong expectation"
driver.quit()
api_reset(base_url)

The following example makes the Failure: teardown skipped after assertion behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Repair: lifecycle ownership is expressed structurally.
with isolated_case(base_url, browser) as driver:
    page = RegistrationPage(driver, base_url)
    page.open()
    assert page.submit(row) == f"created:{row['email']}"

The original failure remains visible; the fixture only guarantees teardown. It must not swallow or replace the assertion exception.

6. Failure: secrets committed or printed

The following example makes the Failure: secrets committed or printed behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Never do this.
TEST_USERS = [{"email": "real-user@example.com", "password": "P@ssw0rd!"}]
print(os.environ)

Use fake synthetic identities and inject credentials only when a disposable authenticated environment truly requires them. Evidence may record token_present=true or a non-sensitive credential alias, never the secret value.

7. Failure: hard-coded environment URL

The following example makes the Failure: hard-coded environment URL behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Brittle and potentially unsafe.
BASE_URL = "https://prod.example.invalid/"
driver.get(BASE_URL)

Hard-coding makes source code choose an environment. The mandatory academy labs use loopback-only targets and reject any override that is not localhost or a loopback IP.

8. Production-target guard

The following example makes the Production-target guard behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from urllib.parse import urlparse
import ipaddress


def require_loopback(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        raise RuntimeError("base URL must be absolute http(s)")
    host = parsed.hostname
    if host == "localhost":
        return url
    try:
        address = ipaddress.ip_address(host)
    except ValueError as exc:
        raise RuntimeError("academy lab refuses non-loopback host") from exc
    if not address.is_loopback:
        raise RuntimeError("academy lab refuses non-loopback host")
    return url

This guard is intentionally stricter than a production suite would be. It makes the learning lab incapable of “falling through” to staging or production. Real organizations can use allowlisted environment IDs and deployment metadata, but the default must remain deny-by-default for destructive tests.

9. Failure: opaque parameter IDs

The following table organizes the key choices and evidence for Failure: opaque parameter IDs. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

Bad Better Diagnostic value
case-17 viewer-invalid-email failure dimension immediately visible
row-3 admin-duplicate-email points to data uniqueness behavior
browser2-data4 firefox-editor-valid matrix tuple visible

Case IDs should not contain secrets or personal data. They describe the behavioral dimension, not the literal user identity.

10. Intentionally broken local example and repair

Use the Lesson 2 fixture but deliberately give two rows the same email. The first row succeeds; the second returns error:duplicate email from the AUT while the test expects created:.... Preserve the browser result and API state.

Observed browser state: error:duplicate email
AUT state: {"users":[{"email":"duplicate@example.test", ...}]}
Interpretation: deterministic data collision, not Selenium timing.
Repair: make case identity unique or reset the AUT per case; do not retry the click.

11. Performance only where causally relevant

Data setup can dominate a suite, but diagnose the layer: browser/session startup, fixture server startup, API seeding, AUT/network latency, evidence I/O, Grid queue, or retry cost. Do not broaden fixture scope until measurement shows startup is the bottleneck and the proposed sharing does not create mutable-state coupling.

12. Production practice

Use environment allowlists, synthetic account namespaces, idempotent cleanup, per-test workspaces, redacted config evidence, and explicit matrix metadata. Make teardown observable: a cleanup failure is a real test-infrastructure failure and should be reported separately from the scenario’s first failure.

13. Summary and next step

State/configuration incidents become actionable when the first evidence includes case identity, environment, seed/data, fixture scope, session provenance, and post-failure AUT state. Fix ownership—not symptoms.

Knowledge check

Two tests use separate browsers but the same mutable account and fail only in parallel. What layer is broken?

Why is a retry dangerous for a duplicate-data collision?

What should happen if teardown fails after the scenario already failed?

Why does the academy checkpoint use a loopback-only URL guard?

What makes a case ID useful?

Next lesson

Prove repeatability twice

The checkpoint builds a parameterized suite with deterministic identities, guarded environment selection, isolated fixture lifecycle, failure-safe cleanup, and two consecutive runs whose business results match.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against Selenium primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0 and Python 3.10+, use Python standard-library unittest for parameterization/fixture examples, use a supported locally installed Chromium-family browser with Selenium Manager, and target only loopback synthetic applications. Parameterization, fixture scope, environment parsing, and secret injection are test-runner/application-infrastructure concerns rather than WebDriver capabilities. The mandatory path deliberately uses Python standard-library unittest rather than requiring pytest. Selenium guidance is applied by keeping tests independent, using fresh browser sessions, and moving repetitive prerequisite/reset work to a disposable lower-layer API when that setup is not the UI behavior under test.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.