← Back to DevBytes

Selenium from Scratch: Practical Guide to

Selenium from Scratch: Practical Guide to Web Automation

Selenium is one of the most powerful and widely used open-source frameworks for automating web browsers. Whether you are building a robust test suite for a complex web application or scraping data from dynamic websites, Selenium provides the tools you need to drive a browser programmatically. This guide walks you through everything you need to know to get started with Selenium from scratch, with practical examples you can run today.

What Is Selenium?

Selenium is a suite of tools used to automate web browsers across different platforms. It supports multiple programming languages including Python, Java, C#, JavaScript, and Ruby. The core components of the Selenium ecosystem are:

For most modern automation tasks, Selenium WebDriver is the component you will use. It communicates with the browser through a driver (such as ChromeDriver or GeckoDriver) that translates your commands into actions performed by the browser.

Why Selenium Matters

Modern web applications are highly interactive and rely heavily on JavaScript to render content dynamically. Traditional HTTP-based scraping tools often fail because they do not execute JavaScript. Selenium solves this problem by controlling an actual browser instance, which means:

For developers and QA engineers, Selenium is the backbone of end-to-end testing. For data engineers, it is a reliable fallback when simpler scraping tools cannot handle dynamic content.

Setting Up Your Environment

This guide uses Python because of its readability and the maturity of its Selenium bindings. The setup process is straightforward.

Installing Selenium

First, ensure you have Python 3 installed. Then install the Selenium package using pip:

pip install selenium

Starting with Selenium 4.6, the Python bindings include Selenium Manager, which automatically downloads and manages browser drivers for you. This means you no longer need to manually download ChromeDriver or GeckoDriver in most cases.

Verifying the Installation

Create a file called verify.py and run the following code to confirm everything works:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://example.com")
print(driver.title)
driver.quit()

If everything is set up correctly, a Chrome window will open, navigate to example.com, print the page title to your console, and then close. If the browser does not open, make sure Google Chrome is installed on your system.

Core Concepts You Must Understand

The WebDriver Object

The WebDriver object is your remote control for the browser. You create an instance of it, and every action you perform — navigating, finding elements, clicking — is a method call on this object. Always call driver.quit() at the end of your script to release browser resources.

Locating Elements

Interacting with a web page requires finding elements first. Selenium provides several locator strategies:

CSS selectors and XPath are the most flexible and commonly used strategies. Here is a comparison:

from selenium.webdriver.common.by import By

# Using CSS selector
search_box = driver.find_element(By.CSS_SELECTOR, "input[name='q']")

# Using XPath
search_box = driver.find_element(By.XPATH, "//input[@name='q']")

Waits: Explicit and Implicit

One of the most common mistakes beginners make is trying to interact with an element before it has loaded. Web pages are asynchronous, and elements appear at unpredictable times. Selenium offers two wait strategies.

Implicit waits tell WebDriver to poll the DOM for a certain amount of time when trying to find an element:

driver.implicitly_wait(10)  # Wait up to 10 seconds

Explicit waits are more precise. They wait for a specific condition to be true before proceeding:

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

wait = WebDriverWait(driver, 10)
button = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "#submit-btn"))
)
button.click()

Explicit waits are strongly preferred because they are deterministic and do not slow down your entire test suite.

Practical Example: Automating a Search

Let us build a complete script that opens a search engine, types a query, submits the form, and extracts the results. This example demonstrates navigation, element location, interaction, and waiting.

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

# Initialize the browser
driver = webdriver.Chrome()

try:
    # Navigate to the search engine
    driver.get("https://www.bing.com")

    # Find the search input and type a query
    search_box = driver.find_element(By.NAME, "q")
    search_box.send_keys("Selenium WebDriver tutorial")
    search_box.send_keys(Keys.RETURN)

    # Wait for results to load
    wait = WebDriverWait(driver, 10)
    results = wait.until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, "h2 a"))
    )

    # Print the top 5 result titles and links
    for i, result in enumerate(results[:5], start=1):
        title = result.text
        link = result.get_attribute("href")
        print(f"{i}. {title}")
        print(f"   URL: {link}")

finally:
    # Always clean up
    driver.quit()

Notice the use of a try/finally block. This ensures the browser is closed even if an exception occurs during execution. This is a critical habit to develop.

Practical Example: Logging Into a Website

Another common automation scenario is logging into a website. The following example shows a generic login flow. Replace the selectors and credentials with values appropriate for your target site.

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

driver = webdriver.Chrome()

try:
    driver.get("https://example.com/login")

    wait = WebDriverWait(driver, 10)

    # Fill in the login form
    username_field = wait.until(
        EC.presence_of_element_located((By.ID, "username"))
    )
    username_field.send_keys("my_username")

    password_field = driver.find_element(By.ID, "password")
    password_field.send_keys("my_secure_password")

    # Submit the form
    login_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
    login_button.click()

    # Wait for the dashboard to appear, confirming login success
    dashboard = wait.until(
        EC.presence_of_element_located((By.ID, "dashboard"))
    )
    print("Login successful!")
    print("Dashboard heading:", dashboard.text)

except Exception as e:
    print(f"An error occurred: {e}")

finally:
    driver.quit()

When automating logins, be mindful of terms of service and rate limits. Many websites prohibit automated access, and repeated login attempts can trigger account lockouts or CAPTCHAs.

Handling Common Challenges

Dealing with Dynamic Content

Single-page applications load content dynamically. Instead of hard-coded sleeps, use explicit waits combined with expected conditions. For example, to wait until an element becomes visible rather than just present in the DOM:

element = wait.until(
    EC.visibility_of_element_located((By.ID, "dynamic-content"))
)

Working with iframes

If an element is inside an <iframe>, you must switch to that frame before interacting with it:

driver.switch_to.frame("frame-name")

# Interact with elements inside the frame
button = driver.find_element(By.ID, "inner-button")
button.click()

# Switch back to the main document
driver.switch_to.default_content()

Handling Alerts and Popups

JavaScript alerts require special handling:

wait.until(EC.alert_is_present())
alert = driver.switch_to.alert
alert.accept()  # or alert.dismiss()

Taking Screenshots

Screenshots are invaluable for debugging failed tests:

driver.save_screenshot("screenshot.png")

Best Practices

Use the Page Object Model

The Page Object Model is a design pattern that separates page structure from test logic. Each page in your application gets a class that encapsulates its elements and actions. This makes your tests more maintainable and readable.

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:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def load(self, url):
        self.driver.get(url)
        return self

    def login(self, username, password):
        self.wait.until(
            EC.presence_of_element_located((By.ID, "username"))
        ).send_keys(username)

        self.driver.find_element(By.ID, "password").send_keys(password)
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        return self


# Usage in a test
driver = webdriver.Chrome()
try:
    page = LoginPage(driver)
    page.load("https://example.com/login").login("user", "pass")
finally:
    driver.quit()

Prefer Explicit Waits Over Implicit Waits

Implicit waits apply globally and can cause confusing behavior when mixed with explicit waits. Stick to explicit waits for predictable, condition-based synchronization.

Avoid Brittle Selectors

Do not rely on auto-generated CSS classes or deeply nested XPath expressions. These change frequently and break your tests. Prefer stable attributes like id, data-testid, or semantic class names.

Run Headlessly in CI

In CI/CD environments, there is no display. Run Chrome in headless mode:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")

driver = webdriver.Chrome(options=options)

Clean Up Resources Reliably

Always use try/finally or context managers to ensure the browser is closed. Lingering browser processes consume memory and can cause port conflicts.

Keep Tests Independent

Each test should set up and tear down its own state. Tests that depend on each other create cascading failures that are difficult to diagnose.

Scaling with Selenium Grid

When your test suite grows, running tests sequentially becomes slow. Selenium Grid lets you distribute tests across multiple machines and browsers. With Docker, you can spin up a grid quickly:

docker run -d -p 4444:4444 --shm-size="2g" selenium/standalone-chrome

Then point your WebDriver to the grid instead of running a local browser:

from selenium import webdriver

options = webdriver.ChromeOptions()
driver = webdriver.Remote(
    command_executor="http://localhost:4444/wd/hub",
    options=options
)

This approach is the foundation for parallel test execution in CI pipelines.

Conclusion

Selenium is a versatile and battle-tested framework for web automation and end-to-end testing. By understanding its core concepts — WebDriver, locators, waits, and the Page Object Model — you can build automation scripts that are reliable, maintainable, and scalable. Start with simple scripts like the search and login examples above, then gradually introduce structure through page objects and parallel execution with Selenium Grid. Remember that the key to successful Selenium automation is not just making the browser do what you want, but doing so in a way that remains stable as your application evolves. With explicit waits, robust selectors, and disciplined resource management, you will be well equipped to handle even the most dynamic web applications.

— Ad —

Google AdSense will appear here after approval

← Back to all articles