Selenium Best Practices
Last updated
Selenium is the most widely used open-source framework for automating web browsers. In its simplest form it is a wrapper around the WebDriver implementations that browser vendors ship, such as ChromeDriver, EdgeDriver, SafariDriver and GeckoDriver.
Most Selenium suites do not fail because the test logic is wrong. They fail because of a small number of design mistakes: blocking sleeps, brittle locators, state leaking between tests and a single driver tied to one browser. The practices below address those directly, in rough order of how much pain they remove.
The short version
- Replace every
sleepwith an explicit wait - Prefer stable locators, ideally a dedicated test attribute
- Put selectors behind a Page Object, not in the test
- Make each test independent and able to run in any order
- Create and clean your own test data
- Run in parallel, and only then across browsers
- Always quit the driver, even when the test fails
- Assert on one behaviour per test
- Name tests so a failure is readable without opening the file
- Capture a screenshot, the page source and browser logs on failure
- Keep tests out of the critical path of a deploy until they are stable
- Retry only known-flaky infrastructure steps, never assertions
- Pin your browser and driver versions together
- Do not automate what an API call can set up faster
- Measure suite duration and flake rate, and treat both as bugs
1. Use explicit waits, never sleeps
A fixed sleep is the single largest source of both slowness and flakiness. It is always either too short, which fails on a slow build, or too long, which wastes time on every run. Wait for the condition you actually care about instead.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-test=checkout]")))
Avoid mixing implicit and explicit waits in the same suite. When both are active the effective timeout becomes hard to reason about and can compound in surprising ways.
2. Choose locators that survive a redesign
A locator that depends on layout will break the first time someone edits the markup. In rough order of durability: a dedicated test attribute, then an id, then a name, then a scoped CSS selector, then XPath.
<button data-test="checkout">Check out</button>
Adding data-test attributes to your application is one of the cheapest reliability improvements available. Absolute XPath expressions such as /html/body/div[3]/div[2]/button are the least durable choice and should be treated as a temporary measure.
3. Put your selectors behind a Page Object
The Page Object Model gives each page or component a class that exposes intent-level methods and hides its selectors. When the UI changes you edit one class rather than every test that touched that screen.
The pattern applies equally to mobile. See Page Object Model with Appium for the Appium version.
4. Keep tests atomic and independent
Each test should set up what it needs and assume nothing about what ran before it. Tests that depend on order cannot be run in parallel, cannot be run individually while debugging, and fail in confusing cascades when an early test breaks.
A quick check: run your suite in a random order. Anything that fails was carrying a hidden dependency.
5. Own your test data
Shared fixtures rot. A test that assumes a particular user already exists will fail the moment someone else changes that user. Create the records you need at the start of the test, ideally through an API rather than the UI, and remove them afterwards.
6. Run in parallel before you run everywhere
Parallel execution is usually the largest single win in suite duration, and it is what makes broad browser coverage affordable. Get the suite running reliably in parallel on one browser first, because parallelism exposes exactly the shared-state problems that cross-browser runs will otherwise hide.
TestingBot provides a grid of remote browsers built for parallel execution, so you do not have to build and maintain the machine configurations yourself.
7. Always quit the driver
An abandoned driver leaks a browser process, and on a shared machine that eventually takes the host down. Quit in a teardown hook that runs even when the test raises.
try:
run_test(driver)
finally:
driver.quit()
8. Assert one behaviour per test
A test with eight assertions reports one failure and hides the other seven. Smaller tests give you a more precise signal and are far easier to retry selectively.
9. Name tests so failures are self-explaining
You should be able to read a failure in a CI summary and know what broke without opening the source. Prefer should_add_to_cart_and_reach_checkout over test_cart_2.
10. Capture evidence on failure
A failed assertion tells you what, not why. Capture a screenshot, the page source and the browser console log at the point of failure and attach them to the run. This turns most flake investigations from a reproduction exercise into a two-minute read.
11. Keep an unstable suite out of the deploy path
A suite that fails randomly trains people to ignore it, and a blocking suite that is ignored is worse than no suite. Run new or unstable tests in a non-blocking lane until their flake rate is low enough to trust.
12. Retry infrastructure, not assertions
Retrying a failed assertion hides real bugs. Retrying a session that never started, or a page that never loaded, is legitimate. Keep the two clearly separated, and log every retry so you can see whether the underlying rate is getting worse.
13. Pin the browser and its driver together
Most mysterious "it worked yesterday" failures are a browser that auto-updated away from its driver. Pin both, and upgrade them as a pair. Google's Chrome for Testing builds exist specifically to make this possible locally.
14. Do not automate setup you can configure
Logging in through the UI on every test is slow and adds a shared point of failure. Where your application allows it, set up state through an API, a seeded session cookie or a feature flag, and reserve UI automation for the behaviour actually under test.
15. Track duration and flake rate
Both tend to degrade quietly until the suite is unusable. Record how long the suite takes and how often it fails without a code change, review the numbers regularly, and treat a regression in either as a defect rather than background noise.
Running these tests across many browser and OS combinations is where a local grid gets expensive to maintain. TestingBot runs your Selenium tests on real browsers and real devices in parallel, with video, logs and HAR files captured for every run, so failure evidence is collected without extra code on your side.