A real test example

A complete login flow test with a page object, explicit waits and assertions that fail for a useful reason.

A page object

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

class LoginPage:
    URL = "https://app.example.com/login"

    def __init__(self, driver, wait):
        self.driver = driver
        self.wait = wait

    def open(self):
        self.driver.get(self.URL)
        return self

    def login(self, email, password):
        self.driver.find_element(By.ID, "email").send_keys(email)
        self.driver.find_element(By.ID, "password").send_keys(password)
        self.driver.find_element(By.CSS_SELECTOR, "[data-testid='login-submit']").click()
        self.wait.until(EC.url_contains("/dashboard"))
        return self

    def error_message(self):
        element = self.wait.until(
            EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='alert']")))
        return element.text

A page object keeps selectors and waits in one place, so a UI change is one edit instead of thirty. Keep assertions out of it: a page object describes what you can do, and the test decides what should be true.

The test itself

import pytest
from selenium.webdriver.support.ui import WebDriverWait

@pytest.fixture
def login_page(driver):
    return LoginPage(driver, WebDriverWait(driver, 10)).open()

def test_valid_credentials_reach_dashboard(login_page):
    login_page.login("[email protected]", "correct-horse-battery")
    assert "/dashboard" in login_page.driver.current_url

def test_bad_password_shows_error(login_page):
    login_page.login("[email protected]", "definitely-wrong")
    assert "incorrect" in login_page.error_message().lower()
  • Assert on user-visible outcomes, not on implementation details such as a CSS class name.
  • Create or reset the data the test needs; a test that depends on run order is not a test.
  • Keep one reason to fail per test, so the test name tells you what broke.
⚠️
Do not automate third-party pages you do not control. A login form behind a bot check or CAPTCHA will fail intermittently and you cannot fix it. Automate your own application and stub the outside world.

FAQ

How do I handle data that persists between tests?
Create what the test needs and delete it afterwards, or give every run a fresh account. Shared state is the second most common cause of flakiness after a missing wait.
Selenium or Playwright?
Playwright has automatic waiting and a built-in runner, so new suites often start there. Selenium remains the safer choice when you must support a specific browser matrix or an existing Java or C# stack.

Locators and waits Install and first test

Last refreshed 2026-09-18.