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.
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.
| Selenium | Playwright | Cypress | |
|---|---|---|---|
| Languages | Java, Python, C#, Ruby, JS | JS/TS, Python, Java, C# | JS/TS only |
| Browsers | All (inc. IE, Safari) | Chrome, Firefox, WebKit | Chrome-family |
| Auto-wait | No (manual waits required) | Yes | Yes |
| Setup complexity | Higher | Low | Low |
| Enterprise adoption | Very high | Growing | Growing |
| Community | Largest | Large | Large |
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 pytestWriting 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_urlLocator 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):
data-testidattribute (add to your app specifically for testing)- ID
- CSS selector (class, attribute)
- Link text
- 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-junitWhat 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
@AfterEachmethod 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.