Checkpoint Lab — Browser Automation and Test Engineering Foundations
Create a tiny browser-automation test charter, implement one reliable happy path and one negative path against a disposable local app, collect an evidence packet, and document which checks belong below the browser.
Learning objectives
- Translate product risk into a two-test browser charter.
- Predict browser/session/DOM/AUT/evidence changes before executing tests.
- Use a fresh WebDriver session for each independent test.
- Capture only safe correlated capability, screenshot, and DOM evidence.
- Run a standard-library test runner with reliable failure exit status.
- Produce cleanup and reproducibility notes that another engineer can follow.
1. Checkpoint acceptance contract
You will prove two user-visible behaviors for a local order fixture: a valid quantity is accepted and an invalid quantity is rejected. You will not use Selenium to load-test the fixture, validate every numeric rule permutation, or create external accounts. The checkpoint is successful only when both tests are independent, use fresh browser sessions, assert visible intended state, write evidence into separate directories, and quit cleanly.
| Risk | Chosen layer | Why |
|---|---|---|
| Visible form accepts valid quantity | Selenium | Browser input + click + visible status are the behavior. |
| Visible form rejects invalid quantity | Selenium | User-visible validation mapping is the behavior. |
| All numeric boundary/rounding rules | Unit/service (documented, not implemented here) | Many data combinations are faster and clearer below the UI. |
| Server under 500 concurrent users | Dedicated load tool | Selenium is not the load generator. |
2. Setup and preflight
The following example makes the Setup and preflight behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
mkdir selenium-ch01-checkpoint
cd selenium-ch01-checkpoint
python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell equivalent: .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
python -c "import sys,selenium; print(sys.version); print('selenium', selenium.__version__)"
mkdir app evidence
Assumptions: Python 3.10+, Selenium Python 4.47.0, one installed supported local browser, Selenium Manager available through the binding, loopback networking, and no Selenium Grid. Before execution, record the browser family you expect; after session creation, trust the returned capabilities for actual browser/version/platform evidence.
3. Create the exact disposable application
Save as app/index.html:
The following example makes the Create the exact disposable application behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Order Charter Lab</title></head>
<body>
<main>
<h1>Order Charter Lab</h1>
<label>Item <input data-testid="item" value="widget" readonly></label>
<label>Quantity <input data-testid="quantity" type="number" min="1" max="9" value="1"></label>
<button data-testid="submit" type="button">Create order</button>
<p data-testid="result" role="status">No order submitted</p>
</main>
<script>
const quantity = document.querySelector('[data-testid="quantity"]');
const result = document.querySelector('[data-testid="result"]');
document.querySelector('[data-testid="submit"]').addEventListener('click', () => {
const q = Number(quantity.value);
setTimeout(() => {
if (!Number.isInteger(q) || q < 1 || q > 9) {
result.textContent = 'Quantity must be between 1 and 9';
result.dataset.state = 'invalid';
return;
}
result.textContent = `Order accepted: widget x ${q}`;
result.dataset.state = 'accepted';
}, 200);
});
</script>
</body>
</html>
The application is intentionally tiny. It has one item, one quantity, one button, and one visible status. The delayed update creates a real synchronization boundary but no external dependency.
4. Start the loopback server
The following example makes the Start the loopback server behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -m http.server 8770 --bind 127.0.0.1 --directory app
Keep this terminal open. Verify the bind address is loopback. If
another process already uses port 8770, choose another local port
and set the same value through SELENIUM_LAB_URL; do not
change the target to a public environment.
5. Write predictions before execution
Record these predictions in your notes before creating the test file:
- Session: each test method creates a different session ID and fresh browser state.
- DOM/AUT: happy path changes the quantity to 3 and eventually sets visible result text/state to accepted; negative path changes it to 0 and eventually sets visible result text/state to invalid.
- Evidence: each test gets its own directory containing safe capability JSON plus PNG/HTML evidence.
-
Cleanup:
addCleanup(driver.quit)releases each session even when an assertion raises.
6. Implement the two-test charter
Save as test_order_charter.py:
The following example makes the Implement the two-test charter behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
import json
import os
import unittest
from pathlib import Path
import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
BASE_URL = os.environ.get("SELENIUM_LAB_URL", "http://127.0.0.1:8770/")
EVIDENCE_ROOT = Path("evidence")
def require_loopback(url: str) -> None:
allowed = ("http://127.0.0.1:", "http://localhost:")
if not url.startswith(allowed):
raise RuntimeError(f"Safety guard refused target: {url}")
class OrderCharterTests(unittest.TestCase):
def setUp(self):
require_loopback(BASE_URL)
self.assertEqual(selenium.__version__, "4.47.0")
self.driver = webdriver.Chrome()
self.addCleanup(self.driver.quit)
self.case_evidence = EVIDENCE_ROOT / self._testMethodName
self.case_evidence.mkdir(parents=True, exist_ok=True)
caps = self.driver.capabilities
safe_caps = {
"selenium": selenium.__version__,
"session_id": self.driver.session_id,
"browserName": caps.get("browserName"),
"browserVersion": caps.get("browserVersion"),
"platformName": caps.get("platformName"),
}
(self.case_evidence / "capabilities.json").write_text(
json.dumps(safe_caps, indent=2), encoding="utf-8"
)
def open_app(self):
self.driver.get(BASE_URL)
self.assertEqual(self.driver.title, "Order Charter Lab")
def wait_result(self, expected: str):
result = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="result"]')
WebDriverWait(self.driver, 3).until(lambda d: result.text == expected)
return result
def save_evidence(self, name: str):
self.driver.save_screenshot(str(self.case_evidence / f"{name}.png"))
(self.case_evidence / f"{name}.html").write_text(
self.driver.page_source, encoding="utf-8"
)
def test_happy_path_accepts_valid_quantity(self):
self.open_app()
quantity = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="quantity"]')
quantity.clear()
quantity.send_keys("3")
self.driver.find_element(By.CSS_SELECTOR, '[data-testid="submit"]').click()
result = self.wait_result("Order accepted: widget x 3")
self.assertEqual(result.get_attribute("data-state"), "accepted")
self.save_evidence("happy")
def test_negative_path_rejects_invalid_quantity(self):
self.open_app()
quantity = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="quantity"]')
quantity.clear()
# send_keys bypasses browser number-input spinner constraints but still exercises the DOM value.
quantity.send_keys("0")
self.driver.find_element(By.CSS_SELECTOR, '[data-testid="submit"]').click()
result = self.wait_result("Quantity must be between 1 and 9")
self.assertEqual(result.get_attribute("data-state"), "invalid")
self.save_evidence("negative")
if __name__ == "__main__":
unittest.main(verbosity=2)
The standard-library unittest runner supplies test
discovery, per-test setup, assertions, reporting, and a non-zero
process exit status when a test fails. Selenium remains the
browser-control layer rather than being mislabeled as the test
runner.
7. Execute and preserve output
The following example makes the Execute and preserve output behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -m unittest -v test_order_charter.py
python - <<'PY'
from pathlib import Path
for p in sorted(Path('evidence').rglob('*')):
if p.is_file():
print(p)
PY
Expected runner output contains two ok results and an
overall OK. Expected evidence includes separate
directories for
test_happy_path_accepts_valid_quantity and
test_negative_path_rejects_invalid_quantity, each with
capabilities.json and its corresponding screenshot/page
source.
8. Verify the predictions independently
-
Compare the two
session_idvalues in capability files; they must differ. -
Open
happy.pngand verifyOrder accepted: widget x 3. -
Open
negative.pngand verifyQuantity must be between 1 and 9. -
Confirm each HTML snapshot contains the matching
data-statevalue. - After the runner exits, verify no intentionally retained browser session remains.
9. Build the checkpoint evidence packet
Your evidence packet should contain:
-
the exact command used to create the virtual environment and
install
selenium==4.47.0; - Python and Selenium versions;
- safe returned capability fields (browser name/version/platform) and session IDs;
- the two test results and process exit status;
- happy/negative screenshots and page-source snapshots from synthetic data only;
- the four predictions above and whether each was confirmed;
- a note that Grid/BiDi/CI were not required in Chapter 01;
- a cleanup record.
10. Prove failure semantics once
In a temporary copy of the test file, change the happy-path expected text to an impossible value. Run only that test. Confirm the runner exits non-zero, the wait/exception is visible, and cleanup still closes the browser. Restore the original file. Do not add retries or extend the timeout to conceal the incorrect expectation.
11. What belongs in Selenium versus lower layers
Document at least three checks you would not add as more browser tests:
- dozens of quantity boundary permutations → unit/service tests;
- raw server response/status contract → HTTP/API test;
- throughput under concurrency → JMeter or another dedicated load tool.
Keep the two browser scenarios because they verify user-visible browser interaction and state mapping. This classification is part of the checkpoint, not optional commentary.
12. Production operating model gained in Chapter 01
You now have a foundation that scales beyond the tiny fixture: explicit test intent, clear ownership boundaries, version/session evidence, loopback/environment guards, fresh browser isolation, condition-based readiness, meaningful assertions, first-failure diagnostics, and deterministic teardown. These are the controls that make later locators, Grid, BiDi, parallelism, and CI useful instead of merely more complex.
13. Cleanup and rollback
The following example makes the Cleanup and rollback behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# Stop the loopback server with Ctrl+C first.
cd ..
# Preserve the checkpoint evidence elsewhere only if required.
rm -rf selenium-ch01-checkpoint
Windows PowerShell equivalent for the final deletion is
Remove-Item -Recurse -Force .\selenium-ch01-checkpoint
after stopping the server. Delete only the disposable lab directory
you created; do not clear global browser profiles, caches, or system
driver directories as part of this course lab.
Knowledge check
Why does each checkpoint test create a new WebDriver instance?
It gives each test a fresh browser/session boundary, reduces hidden browser-state coupling, and makes later parallel execution easier to reason about.
Why are capability files stored per test rather than once globally?
They correlate the environment evidence to the exact session that produced a result. In remote/cross-browser execution, different tests may receive different browsers/nodes.
What failure does the loopback URL guard prevent?
It prevents the educational test from being casually redirected to an external or production-like site where actions or data might be unsafe.
Why should quantity-boundary permutations stay below Selenium?
The business rule can be exercised more completely, quickly, and diagnostically in unit/service tests; Selenium is kept for the browser-visible mapping and interaction risk.
What proves that teardown is part of correctness?
The temporary failing run must still close the browser because cleanup is registered independently of test success. Resource/session lifecycle is part of a reliable automation platform.
What does Chapter 02 add next?
It deepens the Selenium ecosystem and installation model, including how bindings, Selenium Manager, browser drivers, services, and the first WebDriver session are configured and verified.
Official references and version notes
- Selenium 4.47 release notes — release baseline used by this chapter.
- Selenium downloads — current binding and Selenium Server/Grid version status.
- WebDriver getting started — current WebDriver/driver mental model.
- Selenium Manager — automated browser/driver management behavior.
- Waiting strategies — race conditions, implicit waits, explicit waits, and the warning about mixing them.
- Avoid sharing state and Fresh browser per test — isolation guidance.
- Troubleshooting assistance — synchronization and cross-browser diagnostic guidance.
- Selenium Python 4.47.0 package metadata — Python 3.10+ requirement and supported browser families.
Version-sensitive statements were rechecked against current Selenium primary documentation on 2026-08-27. Mandatory examples pin the Python Selenium binding to 4.47.0, require Python 3.10+, use an installed supported local browser with Selenium Manager as the default driver-management path, and do not require Selenium Grid, a paid browser cloud, enterprise identity, or a production website. Record the browser and driver versions returned by the actual session because those remain environment-specific.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.