EthicsComputerCourses
Join training +91 90532 10052

Selenium WebDriver

Software Testing · Lesson 20 of 30 Advanced 7 min read Updated

Selenium WebDriver drives a real browser the way a user would: it opens pages, types into fields, clicks buttons and reads what appears. It has been the standard tool for browser automation for many years, it supports Chrome, Edge, Firefox and Safari, and it has official bindings for Python, Java, JavaScript, C# and Ruby. This lesson uses Selenium 4.52 with Python.

In this lesson you will install Selenium, write a login test against a small page, choose good locators, use explicit waits instead of sleep, and organise browser tests with pytest.

How WebDriver works

Your test code does not control the browser directly. It sends commands through the WebDriver protocol, a W3C standard (WebDriver became a W3C Recommendation in June 2018), to a driver program such as chromedriver or geckodriver, which controls the browser:

Text
Your Python test  --(WebDriver protocol)-->  chromedriver  -->  Chrome

Since Selenium 4.6, a tool called Selenium Manager comes inside Selenium. When no driver is found, it detects your browser version and downloads a matching driver automatically, so you no longer download drivers by hand. Newer releases can even download a browser if none is installed.

Install and the first script

Create a virtual environment for your project and install Selenium and pytest:

Terminal
python -m pip install selenium pytest

The examples test a small login page for a shop called Kirana Kart. After a correct login it shows "Hello, Priya"; after a wrong password it shows "Invalid email or password". Both answers appear about 0.8 seconds after the click, as they would on a real site waiting for its server. Here is the core of a Selenium script:

Python
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()          # Selenium Manager finds or downloads chromedriver
driver.get("http://localhost:8000/login.html")
driver.find_element(By.ID, "email").send_keys("priya@example.com")
driver.find_element(By.ID, "password").send_keys("Kart@2026")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
print(driver.title)
driver.quit()                        # always close the browser

The main commands are get() to open a URL, find_element() to locate one element (it raises NoSuchElementException if none is found), find_elements() to get a list, send_keys() to type, click(), .text to read visible text, and quit() to close the browser and the driver.

Locators

A locator tells Selenium how to find an element. The By class offers:

LocatorExampleNotes
By.ID(By.ID, "email")Fast and stable when IDs are unique
By.NAME(By.NAME, "password")Good for form fields
By.CSS_SELECTOR(By.CSS_SELECTOR, "button[type='submit']")Flexible; the most common choice
By.XPATH(By.XPATH, "//button[text()='Log in']")Powerful, can match text; easy to make fragile
By.LINK_TEXT(By.LINK_TEXT, "Forgot password?")For links
By.CLASS_NAME, By.TAG_NAME(By.TAG_NAME, "h1")Often match many elements

Prefer locators that describe meaning and survive design changes: IDs, names, labels or a dedicated test attribute such as data-testid="cart-count" that developers add for testers. Avoid long absolute paths such as /html/body/div[2]/div/form/div[3]/button; one new div breaks them.

Waits: the number one source of flaky tests

Web pages change after they load: data arrives from servers, messages appear after a delay. If your test reads the message at once, it finds an empty paragraph. Beginners fix this with time.sleep(5), which makes every test slow and still fails when the server takes six seconds.

Selenium offers two kinds of wait:

  • Implicit wait (driver.implicitly_wait(5)) tells every find_element to keep looking for up to 5 seconds. Its default is 0. It only waits for elements to exist, not to be visible or to contain text.
  • Explicit wait (WebDriverWait) waits for a specific condition, such as "this element is visible" or "this text is present", checking again and again until it is true or a timeout expires. Selenium's Python WebDriverWait checks every 0.5 seconds by default.

The Selenium documentation advises against mixing implicit and explicit waits, because the timeouts can combine in surprising ways. Most teams use explicit waits only. This runnable simulation shows exactly what an explicit wait does, using a fake clock so it runs instantly:

PythonRun it yourself
class FakeClock:
    def __init__(self):
        self.now = 0.0
    def sleep(self, seconds):
        self.now += seconds

class FakePage:
    """The error message appears 0.8 s after the Log in click."""
    def __init__(self, clock):
        self.clock = clock
    def message_text(self):
        return "Invalid email or password" if self.clock.now >= 0.8 else ""

def wait_until(condition, clock, timeout=5.0, poll=0.5):
    """Check the condition every `poll` seconds until it is true or time runs out."""
    waited = 0.0
    while True:
        value = condition()
        if value:
            return value, waited
        if waited >= timeout:
            raise TimeoutError(f"condition not met after {timeout} s")
        clock.sleep(poll)
        waited += poll

clock = FakeClock()
page = FakePage(clock)
print("Checked at once:", repr(page.message_text()))

text, waited = wait_until(page.message_text, clock)
print(f"Explicit wait: {text!r} after {waited} s")

clock2 = FakeClock()
try:
    wait_until(lambda: False, clock2, timeout=2.0)
except TimeoutError as error:
    print("Never appeared:", error)
Output
Checked at once: ''
Explicit wait: 'Invalid email or password' after 1.0 s
Never appeared: condition not met after 2.0 s

The wait returns as soon as the message appears (at the second check, 1.0 s), instead of always sleeping 5 seconds, and it fails with a clear timeout when the message never comes.

A pytest suite with Selenium

A pytest fixture starts the browser before each test and quits it afterwards, even when the test fails. expected_conditions (imported as EC) provides ready-made conditions for WebDriverWait:

Python
# test_login_selenium.py
import pytest
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

LOGIN_URL = "http://localhost:8000/login.html"


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")      # no visible window, good for CI
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def log_in(driver, email, password):
    driver.get(LOGIN_URL)
    driver.find_element(By.ID, "email").send_keys(email)
    driver.find_element(By.ID, "password").send_keys(password)
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()


def test_valid_login_opens_dashboard(driver):
    log_in(driver, "priya@example.com", "Kart@2026")
    wait = WebDriverWait(driver, timeout=5)
    wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, "h1"), "Hello, Priya"))
    assert driver.title == "Kirana Kart - Dashboard"


def test_wrong_password_shows_error(driver):
    log_in(driver, "priya@example.com", "wrong-pass")
    message = WebDriverWait(driver, timeout=5).until(
        EC.visibility_of_element_located((By.ID, "message"))
    )
    assert message.text == "Invalid email or password"

Run it with pytest -v:

Output
test_login_selenium.py::test_valid_login_opens_dashboard PASSED          [ 50%]
test_login_selenium.py::test_wrong_password_shows_error PASSED           [100%]

Starting a real browser takes a few seconds per test, so UI suites are much slower than unit tests. This is why you test calculations with unit tests and keep browser tests for real user journeys.

While checking this example, a version of the error test without the explicit wait also passed on a slow laptop, because the browser happened to take longer than 0.8 seconds between the click and the check. On a faster machine or CI server the same test fails. A test that passes or fails without any code change is called flaky, and missing waits are the most common cause.

Common mistakes

  • time.sleep() everywhere. Slow and still unreliable; wait for a condition instead.
  • Fragile XPath copied from the browser. Use IDs, names, labels or data-testid attributes.
  • Forgetting driver.quit(). Leftover browsers eat memory; a fixture with yield guarantees clean-up.
  • Testing everything through the UI. Slow suites get ignored. Keep UI tests for key journeys.

Key points

  • Selenium WebDriver controls real browsers through the W3C WebDriver protocol and a browser driver.
  • Selenium Manager (since 4.6) finds or downloads the matching driver for you.
  • Choose stable locators: IDs, names, labels and data-testid attributes over long XPaths.
  • Use explicit waits (WebDriverWait with expected_conditions) instead of sleep; do not mix them with implicit waits.
  • Start and quit the browser in a pytest fixture, and run headless in CI.

Exercise

The dashboard after login contains <p data-testid="cart-count">Cart: 0 items</p>. Write a test test_new_user_cart_is_empty(driver) that logs in as Priya, waits until this element is visible using a CSS selector for the data-testid attribute, and asserts its text.

Show solution
Python
def test_new_user_cart_is_empty(driver):
    log_in(driver, "priya@example.com", "Kart@2026")
    cart = WebDriverWait(driver, timeout=5).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='cart-count']"))
    )
    assert cart.text == "Cart: 0 items"

The selector [data-testid='cart-count'] matches the attribute exactly. It keeps working if designers change the element's class names, colours or position, which is why teams agree on test IDs with their developers.

Quick Quiz

1. What does Selenium Manager do?

2. Which wait is recommended for a message that appears after a server reply?

3. Which locator is most likely to survive a page redesign?

4. A test passes on one run and fails on the next with no code change. What is it called?

Learn Software Testing with a mentor

Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call