Selenium WebDriver Basics
Your first Selenium test in Python — locators, actions, explicit waits and assertions against a real login form.
What is Selenium WebDriver?
Selenium WebDriver drives a real browser through the W3C WebDriver protocol. Your test code sends commands ("open this URL", "click this button"), and the browser executes them exactly as a user would. It supports Chrome, Firefox, Edge and Safari, and has official bindings for Java, Python, C#, JavaScript and Ruby.
This lesson uses Python + pytest because it's the quickest to read and set up.
Setup
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install selenium pytest
Selenium 4.6+ includes Selenium Manager, which downloads the correct browser driver automatically — no manual chromedriver setup.
Your first test
We'll test the public practice login page at https://the-internet.herokuapp.com/login (username tomsmith, password SuperSecretPassword!).
# test_login.py — run with: pytest -v
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
BASE_URL = "https://the-internet.herokuapp.com"
def test_valid_login_shows_secure_area():
driver = webdriver.Chrome()
try:
driver.get(f"{BASE_URL}/login")
driver.find_element(By.ID, "username").send_keys("tomsmith")
driver.find_element(By.ID, "password").send_keys("SuperSecretPassword!")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
flash = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, "flash"))
)
assert "You logged into a secure area!" in flash.text
assert driver.current_url.endswith("/secure")
finally:
driver.quit()
Run pytest -v and you'll see Chrome open, log in and close.
Locators
Locators tell Selenium how to find elements. In order of preference:
| Strategy | Example | Notes |
|---|---|---|
By.ID |
(By.ID, "username") |
Fast and stable when IDs are unique |
By.CSS_SELECTOR |
(By.CSS_SELECTOR, "[data-test='login-button']") |
Flexible; dedicated data-test attributes are the most stable |
By.NAME |
(By.NAME, "email") |
Good for form fields |
By.LINK_TEXT |
(By.LINK_TEXT, "Logout") |
For links with fixed text |
By.XPATH |
(By.XPATH, "//button[text()='Log in']") |
Most powerful; use when nothing else works |
Avoid brittle locators like /html/body/div[2]/div/form/button — any layout change breaks them. Ask developers to add data-test attributes to important elements.
Waits: the key to stable tests
Pages load asynchronously. Clicking an element before it exists throws NoSuchElementException. There are three ways to wait — only one is good:
import time
time.sleep(5) # ✘ fixed sleep: slow when the page is fast, still fails when it's slower
driver.implicitly_wait(10) # ~ implicit wait: global, can't wait for visibility or clickability
WebDriverWait(driver, 10).until( # ✔ explicit wait
EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))
).click()
Useful expected conditions: visibility_of_element_located, element_to_be_clickable, text_to_be_present_in_element, url_contains, invisibility_of_element_located (for spinners).
Don't mix implicit and explicit waits — the timeouts can combine unpredictably.
Common actions
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import Select
field = driver.find_element(By.ID, "username")
field.clear()
field.send_keys("tomsmith", Keys.TAB) # type, then press Tab
Select(driver.find_element(By.ID, "dropdown")).select_by_visible_text("Option 2")
checkbox = driver.find_element(By.CSS_SELECTOR, "input[type='checkbox']")
if not checkbox.is_selected():
checkbox.click()
driver.save_screenshot("after-login.png") # evidence for reports
A negative test with a pytest fixture
Fixtures remove the repeated browser setup and guarantee the browser closes even when a test fails:
# conftest.py
import pytest
from selenium import webdriver
@pytest.fixture
def driver():
options = webdriver.ChromeOptions()
options.add_argument("--headless=new") # no visible window — needed in CI
browser = webdriver.Chrome(options=options)
yield browser
browser.quit()
# test_login_invalid.py
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_wrong_password_shows_error(driver):
driver.get("https://the-internet.herokuapp.com/login")
driver.find_element(By.ID, "username").send_keys("tomsmith")
driver.find_element(By.ID, "password").send_keys("wrong-password")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
flash = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "flash")))
assert "Your password is invalid!" in flash.text
assert driver.current_url.endswith("/login")
These tests work, but every test repeats the selectors. The Page Object Model fixes that.