What you'll learn
Quick Answer
Browser automation drives a real browser to test complete user journeys. Use explicit waits rather than fixed sleeps, select elements by role or test ID rather than CSS paths, and keep these tests few because they are slow.
What it does and where it fits
Selenium and its modern equivalents — Playwright and Cypress — control a real browser programmatically: navigate, click, type, and assert on what appears.
This tests the thing users actually experience. Unit tests verify a function; a browser test verifies that a person can log in, search, and complete a booking with the real HTML, CSS and JavaScript running.
The trade-off is cost. A unit test runs in milliseconds; a browser test takes seconds. A unit test failing points at one function; a browser test failing tells you the journey broke somewhere.
Hence the testing pyramid: many unit tests, fewer integration tests, a small number of browser tests covering only the critical journeys. Teams that invert this end up with a slow suite nobody trusts. See test-driven development.
A basic test
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
try:
driver.get("https://example.com/login")
driver.find_element(By.NAME, "email").send_keys("asha@example.com")
driver.find_element(By.NAME, "password").send_keys("secret")
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
heading = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "h1"))
)
assert "Dashboard" in heading.text
finally:
driver.quit()
The finally matters — a browser left running after a failed test accumulates until the machine runs out of memory, which is a common problem in CI.
Waiting: the cause of nearly all flakiness
This is the single most important section.
Browser tests are asynchronous by nature. You click submit; the request takes an unknown time; the DOM updates when it completes. Assert too early and the element does not exist yet.
The instinctive fix is wrong:
time.sleep(3) # do not do this
It fails both ways. If the response takes 3.5 seconds — on a loaded CI machine, or a slow day — the test fails despite the application being correct. And when the response takes 200ms, you wasted 2.8 seconds, multiplied across every test in the suite.
Use an explicit wait for the condition you actually care about:
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "h1"))
)
This polls until the condition is true or the timeout expires — returning as soon as it is ready. Fast when the app is fast, patient when it is slow.
A test suite full of sleep calls is the definition of a flaky suite, and flaky tests get ignored, which makes the whole suite worthless.
Selectors that survive a redesign
The second source of maintenance pain. A selector like div > div:nth-child(3) > span.btn-primary breaks the moment anyone adds a wrapper div.
In rough order of preference:
- Accessible role and name — "the button labelled Submit". Tests what a user perceives, and doubles as an accessibility check.
- A dedicated test attribute —
data-testid="submit-booking". Explicit, and safe from styling changes because everyone knows not to touch it. - Stable semantic attributes — an
id, or a form fieldname. - Text content — reasonable, though it breaks with wording or translation changes.
- CSS structure paths — last resort, and the most fragile.
Never select on generated class names such as css-1x2y3z. They change on every build.
Practical guidance
- Consider Playwright for new projects. It waits for elements automatically, which removes most flakiness by default, and handles multiple browsers with less setup. Selenium remains the most widely used and is what many job listings mention.
- Run headless in CI — no visible window, faster, and works on a server with no display.
- Keep tests independent. Each should set up its own state and not depend on another running first, or a single failure cascades and parallel execution becomes impossible.
- Do not test through the UI what you can test below it. Validating twenty form-field error messages through a browser is slow; test the validation logic directly and cover one representative case in the browser.
- Capture a screenshot on failure. A CI failure with no visual evidence is very hard to diagnose, and this takes two lines.
- Fix or delete flaky tests immediately. A test that fails randomly trains the team to re-run rather than investigate, which is how a real failure gets ignored.
