ScreenshotNeo

BlogGuides

Selenium Test Automation: Tips and Best Practices

Build Selenium tests that are faster to diagnose and less flaky with focused test flows, condition-based waits, Page Objects, and reliable browser setup.

By the ScreenshotNeo team4 October 202610 min read

Selenium test automation is most useful when a real browser interaction is part of the behavior you need to verify. Keep each test focused, prepare data outside the browser where possible, wait for the state the next action needs, and isolate browser sessions. These practices make failures easier to understand and keep browser tests from doing work that a faster, lower-level test could cover.

Selenium’s documentation emphasizes that no single approach fits every project. Use browser tests for behavior that depends on a browser; use unit or other lower-level tests when they answer the question with less execution time and infrastructure. Selenium’s test practices and project overview provide the project’s own guidance.

1. Decide what belongs in a Selenium test

Before writing a browser test, ask whether the behavior can be established at a lower test level. Browser tests exercise more moving parts: the browser, the application, network requests, and often a test environment. They are valuable when those interactions are the subject of the test, but expensive when used to prove logic that can be checked without a browser.

Test level Use it when Trade-off
Unit or lower-level test You need to verify isolated application logic or a service contract. Fast and focused, but does not prove browser interaction.
Selenium browser test You need to verify a user-visible flow, browser behavior, or integration across the rendered application. Higher fidelity for browser behavior, with more runtime and infrastructure.

A useful browser test has three parts: set up the required data, perform a discrete set of actions, and evaluate the result. Keep the flow small enough that a failure points to a specific behavior. One long script with many unrelated actions is slower and harder to diagnose.

2. Keep tests independent and focused

A test should establish its own starting conditions and leave the next test able to start cleanly. Avoid depending on a previous test having run or on a shared browser session carrying the right cookies, page state, or data.

  • Create only the data needed for the scenario.
  • Perform only the browser actions relevant to the behavior under test.
  • Assert the outcome that demonstrates the behavior.
  • Close the driver even when the test fails.
  • Use a fresh browser session per test when practical; do not share a driver between unrelated tests.

Isolation makes tests easier to rerun and failures easier to reproduce. If a suite’s framework requires a different session lifetime for resource reasons, make that choice explicit and ensure state is reset between cases.

3. How do I stop Selenium tests from being flaky?

Start by identifying the condition that was not ready when the test acted. A navigation command waiting for document loading does not guarantee that JavaScript-driven content has appeared or become interactive. Race conditions between the browser and test code are a common source of flakiness.

Wait for the application state the next step requires: an element to be present, visible, clickable, or a particular value to appear. Avoid choosing a longer timeout before you know what condition was missing. A longer timeout can mask a synchronization problem and make every failure slower.

Should I use explicit waits or sleep?

Approach What it waits for Failure behavior Runtime effect
Fixed sleep A guessed duration, regardless of page state. Can still proceed too early if the page takes longer than the guess. Always pays the full delay, even when the page is ready sooner.
Explicit condition wait A specific state needed by the next action. Times out if the condition never becomes true. Can proceed as soon as the condition is satisfied.

Prefer an explicit wait for the condition the next line needs. Selenium warns that mixing implicit and explicit waits in one session can produce unpredictable combined timing. Choose one synchronization strategy and apply it consistently. See Selenium’s waits documentation.

4. Complete runnable example in Python

This example uses Python, Selenium 4, and pytest. It opens a local test page, waits for a result element, asserts its text, and closes the browser. Replace the example URL and selectors with those from your application. Selenium Manager can locate or manage a driver when one is not supplied, subject to the environment’s browser and network setup.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


def test_search_shows_results():
    driver = webdriver.Chrome()
    try:
        driver.get("https://example.com/search")
        wait = WebDriverWait(driver, 10)

        search = wait.until(
            EC.visibility_of_element_located((By.NAME, "q"))
        )
        search.send_keys("selenium")
        search.submit()

        result = wait.until(
            EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='search-result']"))
        )
        assert "selenium" in result.text.lower()
    finally:
        driver.quit()

Install the dependencies and run the test:

python -m pip install selenium pytest
pytest -q

Use a test page and result selector that your application actually provides. The example’s domain and selectors are illustrative; it does not claim that a public site implements this search flow.

Using an explicit wait correctly

  • Wait for the state required by the next operation, not a generic notion of “the page is ready.”
  • Use visibility when the user needs to see the element; use presence when DOM existence is enough.
  • Use clickability when the next operation is a click.
  • Give waits a bounded timeout and let a timeout fail with useful test context.
  • Do not combine implicit and explicit waits in the same session.

5. Structure a suite with Page Objects

A Page Object holds knowledge of a page’s structure and exposes page operations that tests need. This centralizes locators and reduces duplicated page knowledge across tests. Keep behavioral assertions in the test; a Page Object may check during construction that the expected page or essential content has loaded.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class SearchPage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
        self.wait.until(
            EC.visibility_of_element_located((By.NAME, "q"))
        )

    def search(self, query):
        field = self.wait.until(
            EC.visibility_of_element_located((By.NAME, "q"))
        )
        field.clear()
        field.send_keys(query)
        field.submit()

    def first_result_text(self):
        result = self.wait.until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, "[data-testid='search-result']")
            )
        )
        return result.text


def test_search_page(driver):
    driver.get("https://example.com/search")
    page = SearchPage(driver)
    page.search("selenium")
    assert "selenium" in page.first_result_text().lower()

The fixture that provides driver should create a fresh session and call quit() during teardown. For repeated regions on a complex page, component objects can encapsulate the repeated structure without turning one Page Object into a large, unrelated collection of helpers.

6. Prepare test state outside the UI

Selenium’s state-generation guidance says Selenium should not be used to prepare a test case. Repeating login screens and data-entry flows in every test adds browser time and creates more opportunities for unrelated failures.

Where the application supports it, use an API, database fixture, or another setup path to create data or establish the required login state. Keep the browser portion focused on the behavior that genuinely needs browser interaction. Preserve realistic authorization and environment boundaries; setup shortcuts should not accidentally bypass the behavior the test is meant to verify.

For example, if a test is about editing a profile, arrange for the user and initial profile through a test API, then use Selenium to open the profile page, edit the target field, save, and verify the displayed result. If the test is specifically about the login flow, perform login in the browser for that test.

7. Manage browser drivers and execution environments

Selenium Manager is included with Selenium releases beginning with version 4.6. Selenium bindings invoke it as a fallback when a driver has not been provided. Teams can also manage browser drivers themselves when they need controlled versions, offline installation, or a specific environment setup. See Selenium Manager documentation.

  1. Pin and document the Selenium binding version used by the project.
  2. Choose whether driver resolution is handled by Selenium Manager or by your environment tooling.
  3. Ensure the browser and driver versions are compatible in CI.
  4. When driver startup fails, inspect the browser installation, available network access, and driver-resolution logs before changing test code.

When should I use Selenium Grid?

Use Grid when you need distributed test execution or browser and operating-system coverage across machines. A small local suite does not need Grid by default. Compare the value of parallel or cross-environment runs against the cost of configuring, maintaining, and diagnosing a distributed system. Selenium’s Grid documentation describes its distributed execution model.

Choice Best fit Cost to consider
Local execution Developing a small suite and debugging a single browser flow. Limited parallelism and environment coverage.
Selenium Grid Distributing runs or covering multiple browser and OS combinations. Additional setup, capacity planning, and failure diagnosis.

8. Troubleshooting common Selenium failures

Symptom Likely cause Fix
NoSuchElementException The locator is wrong, the element has not been added yet, or the test is on an unexpected page. Confirm the current URL and locator in the rendered page; wait for the relevant element state before accessing it.
TimeoutException The awaited condition never became true within the timeout. Check the selector, expected page state, application response, and whether an overlay blocks the target. Increase the timeout only when the legitimate operation can take longer.
StaleElementReferenceException The page re-rendered or replaced the element after it was located. Locate the element again after the update and wait for the post-update state instead of reusing an old reference.
Click intercepted or element not interactable An overlay, animation, off-screen position, or disabled control prevents interaction. Wait for the overlay to disappear or the control to become clickable; scroll or change the test setup only when that matches real user behavior.
Test passes locally but fails in CI Different browser versions, slower responses, missing dependencies, or environment-specific state. Record browser and Selenium versions, verify driver resolution and network access, and wait for the needed state rather than adding a blind sleep.
Driver cannot start Browser missing, incompatible driver, or Selenium Manager cannot resolve what is needed. Install the intended browser, check version compatibility and network policy, or provide a driver through the project’s environment tooling.
Tests affect each other Shared sessions, test data, cookies, or ordering assumptions. Use independent data and sessions where practical; ensure cleanup runs and avoid relying on test execution order.

When diagnosing a failure, capture the failing URL, browser and driver versions, test input, and the specific condition that timed out. This evidence helps distinguish a locator issue from a slow application response or a broken test environment.

9. Performance, reliability, and cost

  • Keep browser coverage intentional. Run browser tests for browser-dependent behavior and cover isolated logic at lower levels.
  • Reduce repeated UI setup. Create test data and login state outside the browser when the application supports a safe setup route.
  • Wait on conditions. Condition waits avoid paying a fixed sleep when the application is ready early, while still providing a bounded failure when it is not ready.
  • Scale only for a need. Grid can support distributed and cross-platform runs, but introduces infrastructure that a small local suite may not need.
  • Make failures reproducible. Independent tests, fresh sessions, deterministic data, and clear assertions help keep retries from hiding genuine defects.

There is no universal best-practice recipe: Selenium’s own documentation says approaches depend on the situation. Tune browser coverage and infrastructure to the behaviors and environments the product must support.

10. ScreenshotNeo for screenshots outside test automation

Selenium is appropriate when the test must interact with a live browser. For a standalone website screenshot, screenshot review, or an AI agent that needs a page image, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its API returns PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation.

Or skip the browser setup:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture, and lets you turn each cleanup step off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; all plans include every feature. Get 1,000 free screenshots a month with no card.

11. Frequently asked questions

Is Selenium suitable for every kind of test?

No. Use it when browser behavior is part of what you need to verify. Prefer lower-level tests when they establish the same behavior with less runtime and infrastructure.

Do Page Objects contain assertions?

Usually, behavioral assertions belong in test code. A Page Object can check that the expected page or essential content is ready when it is created.

Does Selenium Manager replace all driver management?

No. It can resolve a missing driver as a fallback, while teams may still provide and manage drivers directly for their environment.

Do I need Selenium Grid to start?

No. Grid is useful for distributed execution and broader browser or operating-system coverage when those needs justify its infrastructure.