Back to blog
Test Automation#selenium#test-automation#java#webdriver#ui-testing

Getting Started with Selenium: A Beginner's Complete Guide

Everything you need to start automating web tests with Selenium WebDriver — setup, writing your first tests, handling dynamic elements, the Page Object Model, and running tests in CI.

InnovateBits6 min read

Selenium has been the backbone of web test automation for over 15 years. Despite newer tools like Playwright and Cypress gaining popularity, Selenium remains the most widely used automation framework in the world — and for good reason. Its cross-browser support, multi-language bindings (Java, Python, C#, Ruby, JavaScript), and huge community make it a safe, well-understood choice for enterprise environments.

This guide walks you through everything you need to start with Selenium WebDriver, from zero to a running CI pipeline.


Selenium vs Modern Alternatives

Before committing to Selenium, it's worth understanding where it sits relative to Playwright and Cypress.

SeleniumPlaywrightCypress
LanguagesJava, Python, C#, Ruby, JSJS/TS, Python, Java, C#JS/TS only
BrowsersAll (inc. IE, Safari)Chrome, Firefox, WebKitChrome-family
Auto-waitNo (manual waits required)YesYes
Setup complexityHigherLowLow
Enterprise adoptionVery highGrowingGrowing
CommunityLargestLargeLarge

Choose Selenium if: your organisation uses Java, C#, or Python and has existing Selenium investment, you need to test on Internet Explorer or real Safari, or your compliance requirements dictate a specific tool.

Consider Playwright if: you're starting fresh, especially on a JavaScript/TypeScript stack. Playwright's auto-waiting and modern API significantly reduce flakiness.


Setup

Java + Maven

Add Selenium to your pom.xml:

<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.18.1</version>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>5.8.0</version>
  </dependency>
</dependencies>

WebDriverManager handles browser driver downloads automatically — no more manually downloading chromedriver.exe.

Python + pip

pip install selenium pytest

Writing Your First Test

Java

import io.github.bonigarcia.wdm.WebDriverManager;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
 
public class FirstTest {
 
    private WebDriver driver;
 
    @BeforeEach
    void setUp() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        driver.manage().window().maximize();
    }
 
    @AfterEach
    void tearDown() {
        if (driver != null) driver.quit();
    }
 
    @Test
    void homepageHasCorrectTitle() {
        driver.get("https://www.innovatebits.com");
        Assertions.assertTrue(driver.getTitle().contains("InnovateBits"));
    }
 
    @Test
    void blogLinkNavigatesToBlog() {
        driver.get("https://www.innovatebits.com");
        driver.findElement(By.linkText("Blog")).click();
        Assertions.assertTrue(driver.getCurrentUrl().contains("/blog"));
    }
}

Python

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
 
@pytest.fixture
def driver():
    d = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    d.maximize_window()
    yield d
    d.quit()
 
def test_homepage_title(driver):
    driver.get("https://www.innovatebits.com")
    assert "InnovateBits" in driver.title
 
def test_blog_navigation(driver):
    driver.get("https://www.innovatebits.com")
    driver.find_element(By.LINK_TEXT, "Blog").click()
    assert "/blog" in driver.current_url

Locator Strategies

Finding the right element is the core skill in Selenium. Selenium 4 supports several strategies:

// By ID — fastest, most reliable when available
driver.findElement(By.id("submit-button"));
 
// By CSS selector — flexible and widely used
driver.findElement(By.cssSelector(".btn-primary"));
driver.findElement(By.cssSelector("input[data-testid='email-input']"));
driver.findElement(By.cssSelector("div.container > button:first-child"));
 
// By XPath — powerful but verbose; use as last resort
driver.findElement(By.xpath("//button[@type='submit']"));
driver.findElement(By.xpath("//h1[contains(text(), 'Welcome')]"));
 
// By link text
driver.findElement(By.linkText("Sign In"));
 
// Selenium 4: Relative locators — position-based
import static org.openqa.selenium.support.locators.RelativeLocator.with;
driver.findElement(with(By.tagName("input")).below(By.id("username-label")));

Locator priority (best to worst):

  1. data-testid attribute (add to your app specifically for testing)
  2. ID
  3. CSS selector (class, attribute)
  4. Link text
  5. XPath (avoid if possible)

Handling Dynamic Elements and Waits

The most common source of Selenium test failures is timing — trying to interact with an element before it's ready. Selenium does not auto-wait like Playwright does, so you must manage waits explicitly.

Never use Thread.sleep()

// ❌ Never do this
Thread.sleep(3000);
driver.findElement(By.id("result")).click();

Thread.sleep is the leading cause of slow, flaky Selenium tests.

Explicit waits (correct approach)

import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
 
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
 
// Wait until element is visible
WebElement element = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("result"))
);
 
// Wait until element is clickable
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit-btn"))).click();
 
// Wait until URL changes
wait.until(ExpectedConditions.urlContains("/dashboard"));
 
// Wait until text appears
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.id("status-message"), "Success"
));

Implicit waits (use sparingly)

// Set once at driver initialisation
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));

Implicit waits apply globally to every findElement call. They can hide slow pages and interact unpredictably with explicit waits. Use explicit waits where you need to wait for specific conditions.


Page Object Model

Just like Playwright, Selenium tests benefit enormously from Page Object Model:

// pages/LoginPage.java
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.*;
import java.time.Duration;
 
public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;
 
    private final By emailField = By.id("email");
    private final By passwordField = By.id("password");
    private final By submitButton = By.cssSelector("button[type='submit']");
    private final By errorMessage = By.cssSelector(".error-alert");
 
    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }
 
    public void navigate() {
        driver.get("https://yourapp.com/login");
    }
 
    public void login(String email, String password) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(emailField))
            .sendKeys(email);
        driver.findElement(passwordField).sendKeys(password);
        driver.findElement(submitButton).click();
    }
 
    public String getErrorMessage() {
        return wait.until(
            ExpectedConditions.visibilityOfElementLocated(errorMessage)
        ).getText();
    }
}
 
// Test using the page object
class LoginTest {
    @Test
    void invalidCredentialsShowError() {
        LoginPage loginPage = new LoginPage(driver);
        loginPage.navigate();
        loginPage.login("user@example.com", "wrongpassword");
        Assertions.assertEquals("Invalid credentials", loginPage.getErrorMessage());
    }
}

Running Tests in Parallel

TestNG (popular Selenium alternative to JUnit) supports parallel execution via its XML configuration:

<!-- testng.xml -->
<suite name="RegressionSuite" parallel="classes" thread-count="4">
  <test name="UITests">
    <classes>
      <class name="tests.LoginTest"/>
      <class name="tests.CheckoutTest"/>
      <class name="tests.SearchTest"/>
    </classes>
  </test>
</suite>

For thread-safe parallel execution, use ThreadLocal to manage driver instances:

public class BaseTest {
    private static final ThreadLocal<WebDriver> driverThreadLocal = new ThreadLocal<>();
 
    protected WebDriver getDriver() {
        return driverThreadLocal.get();
    }
 
    @BeforeMethod
    public void setUp() {
        WebDriverManager.chromedriver().setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless", "--no-sandbox", "--disable-dev-shm-usage");
        driverThreadLocal.set(new ChromeDriver(options));
    }
 
    @AfterMethod
    public void tearDown() {
        if (getDriver() != null) {
            getDriver().quit();
            driverThreadLocal.remove();
        }
    }
}

CI/CD Integration

GitHub Actions (Java + Maven)

name: Selenium Tests
on: [push, pull_request]
 
jobs:
  selenium:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: maven
      - name: Run Selenium tests
        run: mvn test -Dheadless=true
      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Selenium Results
          path: target/surefire-reports/*.xml
          reporter: java-junit

What to Try Next

With Selenium running locally and in CI, the natural next steps are:

  • Cross-browser testing with Sauce Labs or BrowserStack for real device/browser combinations
  • Screenshot on failure for better debugging — add a @AfterEach method that captures a screenshot on test failure
  • Reporting with Allure or Extent Reports for rich HTML reports
  • Comparing Selenium to Playwright for new projects — see our Playwright guide to understand the tradeoffs

For demo sites to practice your Selenium skills before testing on your own app, see our UI Automation Testing Demo Websites guide.