Selenium #
Manual web interface testing is boring, inconsistent, and doesn’t scale. A developer can spend hours clicking through forms, filling inputs, and verifying results — and still miss edge cases. Selenium WebDriver solves this: it lets you control real browsers (Chrome, Firefox, Edge, Safari) programmatically from Java code. Every click, form fill, page navigation, and content verification can be written as code that runs repeatedly with consistent results. Beyond testing, Selenium is also used for web scraping — extracting data from pages that need JavaScript to render their content, something a plain HTTP client can’t do. In this article, we’ll build a solid foundation for writing reliable, maintainable Selenium tests, free of the most common anti-patterns that make Selenium tests slow and flaky.
How Selenium WebDriver Works #
Selenium WebDriver communicates with the browser through the WebDriver protocol (a W3C standard). Each browser has its own driver that acts as the bridge between Java code and the browser.
flowchart LR
JAVA["Java Code\nSelenium WebDriver"] -->|WebDriver Protocol\nHTTP/JSON| DRIVER["ChromeDriver /\nGeckoDriver /\nEdgeDriver"]
DRIVER -->|DevTools Protocol\nor native API| BROWSER["Browser\nChrome / Firefox / Edge"]
BROWSER -->|DOM events| DRIVER
DRIVER -->|JSON response| JAVASelenium 4 introduced Selenium Manager, which eliminates the need to download and configure drivers manually — the library automatically detects the installed browser version and downloads the matching driver.
Setting Up Dependencies #
<!-- Maven -->
<dependencies>
<!-- Selenium WebDriver -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.21.0</version>
</dependency>
<!-- JUnit 5 as the test runner -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for expressive assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
// Gradle
dependencies {
implementation 'org.seleniumhq.selenium:selenium-java:4.21.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testImplementation 'org.assertj:assertj-core:3.25.3'
}
Initializing the WebDriver #
Selenium 4 includes a built-in Selenium Manager that automatically downloads and configures the matching driver — you no longer need to download chromedriver.exe manually or use a third-party WebDriverManager.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.edge.EdgeDriver;
public class WebDriverFactory {
// Chrome — the most commonly used driver
public static WebDriver createChrome() {
ChromeOptions options = new ChromeOptions();
// Options useful for CI/CD environments
options.addArguments("--no-sandbox"); // required in Docker/Linux
options.addArguments("--disable-dev-shm-usage"); // prevents crashes in containers
options.addArguments("--window-size=1920,1080");
// ✗ ANTI-PATTERN: the old headless mode — doesn't render some CSS features
// options.addArguments("--headless");
// ✓ CORRECT: the new headless mode (Selenium 4.8+) — more accurate
options.addArguments("--headless=new");
return new ChromeDriver(options);
}
// Firefox
public static WebDriver createFirefox() {
FirefoxOptions options = new FirefoxOptions();
options.addArguments("--headless");
options.addArguments("--width=1920");
options.addArguments("--height=1080");
return new FirefoxDriver(options);
}
// Create a driver based on an environment variable
// Useful for running tests on different browsers without changing code
public static WebDriver createFromEnv() {
String browser = System.getenv().getOrDefault("BROWSER", "chrome");
return switch (browser.toLowerCase()) {
case "firefox" -> createFirefox();
case "edge" -> new EdgeDriver();
default -> createChrome();
};
}
}
Basic Setup and Teardown #
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import java.time.Duration;
public class BaseTest {
protected WebDriver driver;
@BeforeEach
void setUp() {
driver = WebDriverFactory.createChrome();
// Implicit wait — how long the driver waits for elements to appear by default
// ✗ ANTI-PATTERN: a high implicit wait — slows tests down when elements don't exist
// driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
// ✓ CORRECT: a short implicit wait, use explicit waits for specific conditions
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));
// Page load timeout
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
// Maximize the browser window
driver.manage().window().maximize();
}
@AfterEach
void tearDown() {
// REQUIRED: close the browser after every test
// Otherwise browsers keep accumulating and consuming memory
if (driver != null) {
driver.quit(); // quit() closes ALL windows + kills the driver process
// Don't use driver.close() — it only closes the active tab
}
}
}
Finding Elements — Locator Strategies #
Choosing the right locator is a core Selenium skill. Bad locators make tests fragile (flaky) — they can pass today and fail tomorrow because of small page changes.
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
public class LocatorDemo {
public void locatorExamples(WebDriver driver) {
// ✗ AVOID: absolute XPath — breaks when the HTML structure changes
driver.findElement(By.xpath("/html/body/div[1]/div[2]/form/input[1]"));
// ✗ AVOID: classes auto-generated by CSS frameworks (random numbers)
driver.findElement(By.className("css-1x7ibqp")); // unstable
// ✓ BEST: ID — always unique on a page, fastest
driver.findElement(By.id("username"));
// ✓ VERY GOOD: data-testid — a testing-specific attribute, doesn't change with styling
driver.findElement(By.cssSelector("[data-testid='login-button']"));
// ✓ GOOD: CSS selectors — faster than XPath, more flexible than ID
driver.findElement(By.cssSelector("form.login-form input[type='email']"));
driver.findElement(By.cssSelector("button[type='submit']"));
// ✓ GOOD: name attribute — common for form inputs
driver.findElement(By.name("password"));
// ✓ ALLOWED: relative XPath with text — for elements without an ID/name
driver.findElement(By.xpath("//button[contains(text(),'Sign In')]"));
driver.findElement(By.xpath("//label[text()='Email']/following-sibling::input"));
// ✓ GOOD: link text — for anchor tags
driver.findElement(By.linkText("Forgot password?"));
driver.findElement(By.partialLinkText("Forgot"));
}
}
Locator priority order, from best to worst:
1. ID → By.id("...") — unique, fast, stable
2. data-testid / data-cy → By.cssSelector("[data-testid='...']") — made for testing
3. name → By.name("...") — good for forms
4. CSS selector → By.cssSelector("...") — flexible, fast
5. Link text → By.linkText("...") — anchors only
6. Relative XPath → By.xpath("//...") — flexible, slower
7. Absolute XPath → /html/body/div/... — AVOID — very fragile
Basic Element Interactions #
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.support.ui.Select;
public class BasicInteractions {
public void demo(WebDriver driver) {
driver.get("https://example.com/login");
// Click an element
WebElement loginButton = driver.findElement(By.id("btn-login"));
loginButton.click();
// Fill a text input
WebElement emailInput = driver.findElement(By.id("email"));
emailInput.clear(); // clear first if there's an old value
emailInput.sendKeys("[email protected]");
// Press keyboard keys
emailInput.sendKeys(Keys.TAB); // move to the next field
driver.findElement(By.id("password")).sendKeys("secret123" + Keys.ENTER);
// Get element text
WebElement message = driver.findElement(By.className("success-message"));
String text = message.getText();
System.out.println("Message: " + text);
// Get attributes
WebElement image = driver.findElement(By.tagName("img"));
String src = image.getAttribute("src");
String altText = image.getAttribute("alt");
// Check element conditions
boolean displayed = loginButton.isDisplayed();
boolean enabled = loginButton.isEnabled();
boolean checked = driver.findElement(By.id("checkbox-agree")).isSelected();
// Dropdowns with Select
WebElement dropdownEl = driver.findElement(By.id("city"));
Select dropdown = new Select(dropdownEl);
dropdown.selectByVisibleText("Jakarta"); // select by text
dropdown.selectByValue("jkt"); // select by value
dropdown.selectByIndex(2); // select by order
String currentSelection = dropdown.getFirstSelectedOption().getText();
// Multiple select
if (dropdown.isMultiple()) {
dropdown.selectByVisibleText("Bandung");
dropdown.selectByVisibleText("Surabaya");
}
// Navigation
driver.navigate().to("https://example.com/dashboard");
driver.navigate().back();
driver.navigate().forward();
driver.navigate().refresh();
// Get page info
String pageTitle = driver.getTitle();
String currentUrl = driver.getCurrentUrl();
System.out.println("Page: " + pageTitle + " | URL: " + currentUrl);
}
}
Explicit Waits — Waiting Intelligently #
This is the most important concept for reliable Selenium tests. Modern web pages are asynchronous — elements appear after an AJAX request finishes, an animation, or other conditions. Waiting with a fixed time (Thread.sleep) is an anti-pattern.
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;
public class ExplicitWaitDemo {
public void demo(WebDriver driver) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// WebDriverWait polls every 500ms until the condition is met or times out
// ✗ ANTI-PATTERN: Thread.sleep — waits a fixed time, regardless of conditions
// If the app is slow, the test fails. If it's fast, time is wasted.
try { Thread.sleep(3000); } catch (InterruptedException e) { }
// ✓ CORRECT: wait until the element is in the DOM (not necessarily visible)
WebElement element = wait.until(
ExpectedConditions.presenceOfElementLocated(By.id("result"))
);
// ✓ CORRECT: wait until the element is visible and clickable
WebElement button = wait.until(
ExpectedConditions.elementToBeClickable(By.id("btn-submit"))
);
// ✓ CORRECT: wait until specific text appears in an element
wait.until(ExpectedConditions.textToBePresentInElementLocated(
By.id("status"), "Saved successfully"
));
// ✓ CORRECT: wait until the URL changes (after form submission)
wait.until(ExpectedConditions.urlContains("/dashboard"));
// ✓ CORRECT: wait until an element DISAPPEARS (loading spinner)
wait.until(ExpectedConditions.invisibilityOfElementLocated(
By.className("loading-spinner")
));
// ✓ CORRECT: wait until the number of elements in a list grows
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
By.cssSelector(".product-item"), 0
));
// Custom conditions — for cases not covered by built-in ExpectedConditions
wait.until(d -> {
String text = d.findElement(By.id("counter")).getText();
return Integer.parseInt(text) > 5;
});
// Handling timeout gracefully
try {
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("optional")));
} catch (TimeoutException e) {
System.out.println("The optional element didn't appear, continuing the test");
}
}
}
The Page Object Model (POM) #
The Page Object Model is a mandatory design pattern for maintainable Selenium tests. Instead of writing locators and actions directly in tests, encapsulate all page details in a separate class. Tests become cleaner, easier to read, and easy to update when the UI changes.
flowchart LR
subgraph TEST[Test Class]
T1["LoginTest\nhome → login → verify"]
end
subgraph POM[Page Objects]
P1["LoginPage\nusernameField\npasswordField\nloginButton\nmessage()"]
P2["DashboardPage\nwelcomeTitle\nlogoutButton\nuserName()"]
end
TEST -->|uses| POM
POM -->|wraps| SELENIUM[Selenium WebDriver API]import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
// Page Object for the login page
public class LoginPage {
private final WebDriver driver;
private final WebDriverWait wait;
// @FindBy — an alternative to driver.findElement(By.xxx)
// Elements are found lazily (only when accessed, not at PageFactory.initElements)
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "button[type='submit']")
private WebElement signInButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
@FindBy(css = ".success-message")
private WebElement successMessage;
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Initialize all @FindBy fields
PageFactory.initElements(driver, this);
}
// Factory method — navigate to the page and return an instance
public static LoginPage open(WebDriver driver) {
driver.get("https://example.com/login");
return new LoginPage(driver);
}
// Atomic actions — one operation per method
public LoginPage fillEmail(String email) {
wait.until(ExpectedConditions.elementToBeClickable(emailInput));
emailInput.clear();
emailInput.sendKeys(email);
return this; // method chaining
}
public LoginPage fillPassword(String password) {
passwordInput.clear();
passwordInput.sendKeys(password);
return this;
}
public DashboardPage clickSignIn() {
signInButton.click();
// Return the Page Object of the next page
return new DashboardPage(driver);
}
// For failed login cases — stays on the login page
public LoginPage clickSignInExpectingFailure() {
signInButton.click();
wait.until(ExpectedConditions.visibilityOf(errorMessage));
return this;
}
// Combined method — a complete login in one call
public DashboardPage login(String email, String password) {
return fillEmail(email)
.fillPassword(password)
.clickSignIn();
}
// Getters for test verification
public String getErrorMessage() {
wait.until(ExpectedConditions.visibilityOf(errorMessage));
return errorMessage.getText();
}
public boolean hasErrorMessage() {
try {
return errorMessage.isDisplayed();
} catch (org.openqa.selenium.NoSuchElementException e) {
return false;
}
}
}
// Page Object for the dashboard page
public class DashboardPage {
private final WebDriver driver;
private final WebDriverWait wait;
@FindBy(css = "h1.welcome-title")
private WebElement welcomeTitle;
@FindBy(id = "user-name")
private WebElement userName;
@FindBy(id = "btn-logout")
private WebElement logoutButton;
@FindBy(css = ".nav-menu a")
private java.util.List<WebElement> menuItems;
public DashboardPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
PageFactory.initElements(driver, this);
// Verify that we're actually on the dashboard page
wait.until(ExpectedConditions.urlContains("/dashboard"));
}
public String getUserName() {
wait.until(ExpectedConditions.visibilityOf(userName));
return userName.getText();
}
public String getTitle() {
return welcomeTitle.getText();
}
public LoginPage logout() {
logoutButton.click();
return new LoginPage(driver);
}
public boolean isLoggedIn() {
try {
return userName.isDisplayed();
} catch (org.openqa.selenium.NoSuchElementException e) {
return false;
}
}
}
Clean Tests with POM #
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class LoginTest extends BaseTest {
private static final String VALID_EMAIL = "[email protected]";
private static final String VALID_PASSWORD = "secret123";
@Test
void loginSucceeds_withValidCredentials() {
// Arrange + Act — very readable thanks to POM
DashboardPage dashboard = LoginPage
.open(driver)
.login(VALID_EMAIL, VALID_PASSWORD);
// Assert
assertThat(dashboard.isLoggedIn()).isTrue();
assertThat(dashboard.getUserName()).contains("User");
assertThat(driver.getCurrentUrl()).contains("/dashboard");
}
@Test
void loginFails_withWrongPassword() {
LoginPage loginPage = LoginPage
.open(driver)
.fillEmail(VALID_EMAIL)
.fillPassword("wrong-password")
.clickSignInExpectingFailure();
assertThat(loginPage.hasErrorMessage()).isTrue();
assertThat(loginPage.getErrorMessage()).contains("Email or password is incorrect");
assertThat(driver.getCurrentUrl()).contains("/login");
}
@Test
void loginFails_withEmptyEmail() {
LoginPage loginPage = LoginPage
.open(driver)
.fillPassword(VALID_PASSWORD)
.clickSignInExpectingFailure();
assertThat(loginPage.getErrorMessage()).contains("Email is required");
}
}
The Actions API — Complex Interactions #
The Actions API is used for interactions that can’t be done with plain click() and sendKeys().
import org.openqa.selenium.interactions.Actions;
public class ActionsDemo {
public void demo(WebDriver driver) {
Actions actions = new Actions(driver);
// Hover (mouse over) — to reveal dropdowns or tooltips
WebElement mainMenu = driver.findElement(By.id("products-menu"));
actions.moveToElement(mainMenu).perform();
// Right click (context menu)
WebElement element = driver.findElement(By.id("item-1"));
actions.contextClick(element).perform();
// Double click
actions.doubleClick(element).perform();
// Drag and drop
WebElement source = driver.findElement(By.id("drag-item"));
WebElement target = driver.findElement(By.id("drop-area"));
actions.dragAndDrop(source, target).perform();
// Drag with an offset (move a number of pixels)
actions.dragAndDropBy(source, 200, 0).perform();
// Press and hold keyboard keys
WebElement searchInput = driver.findElement(By.id("search-input"));
searchInput.sendKeys("laptop");
actions.keyDown(Keys.CONTROL)
.sendKeys("a") // Ctrl+A — select all
.keyUp(Keys.CONTROL)
.perform();
// Scroll to a specific element (Selenium 4)
WebElement farElement = driver.findElement(By.id("footer"));
actions.scrollToElement(farElement).perform();
// Scroll by a number of pixels
actions.scrollByAmount(0, 500).perform(); // scroll down 500px
// Complex action sequences — chain multiple actions
actions.moveToElement(mainMenu)
.pause(Duration.ofMillis(500)) // wait for the dropdown to appear
.moveToElement(driver.findElement(By.linkText("Laptop")))
.click()
.perform();
}
}
The JavaScript Executor #
For actions that can’t be done through the regular WebDriver, use the JavaScript Executor:
import org.openqa.selenium.JavascriptExecutor;
public class JavaScriptDemo {
public void demo(WebDriver driver) {
JavascriptExecutor js = (JavascriptExecutor) driver;
// Scroll to specific coordinates
js.executeScript("window.scrollTo(0, document.body.scrollHeight)");
js.executeScript("window.scrollTo(0, 0)"); // scroll to the top
// Click an element that can't be clicked via WebDriver
// (hidden behind another element, or has a special event listener)
WebElement button = driver.findElement(By.id("special-btn"));
js.executeScript("arguments[0].click();", button);
// Fill an input that rejects sendKeys (e.g. file inputs or readonly)
WebElement input = driver.findElement(By.id("date-input"));
js.executeScript("arguments[0].value = arguments[1];", input, "2024-12-31");
// Highlight an element for visual debugging
js.executeScript(
"arguments[0].style.border='3px solid red'", button
);
// Get values from JavaScript
String pageTitle = (String) js.executeScript("return document.title;");
Long pageHeight = (Long) js.executeScript(
"return document.body.scrollHeight;"
);
// Remove an attribute blocking interaction (e.g. readonly)
js.executeScript("arguments[0].removeAttribute('readonly')", input);
}
}
Screenshots and Reporting #
Taking a screenshot when a test fails is very helpful for debugging. Integrate it into the JUnit 5 lifecycle:
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.OutputType;
import org.junit.jupiter.api.extension.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
// A JUnit 5 Extension for automatic screenshots when tests fail
public class ScreenshotExtension implements TestWatcher {
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
// Get the WebDriver from the test class
Object testInstance = context.getRequiredTestInstance();
if (testInstance instanceof BaseTest baseTest && baseTest.driver != null) {
takeScreenshot(baseTest.driver, context.getDisplayName());
}
}
private void takeScreenshot(WebDriver driver, String testName) {
try {
File screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
String time = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = testName.replaceAll("[^a-zA-Z0-9]", "_")
+ "_" + time + ".png";
Path destination = Paths.get("target", "screenshots", fileName);
Files.createDirectories(destination.getParent());
Files.copy(screenshot.toPath(), destination);
System.out.println("Screenshot saved: " + destination);
} catch (IOException e) {
System.err.println("Failed to save screenshot: " + e.getMessage());
}
}
}
// Use the extension in BaseTest
@ExtendWith(ScreenshotExtension.class)
public class BaseTest {
public WebDriver driver; // public so the extension can access it
@BeforeEach
void setUp() { driver = WebDriverFactory.createChrome(); }
@AfterEach
void tearDown() { if (driver != null) driver.quit(); }
}
Handling Special Cases #
Alerts, Popups, and Frames #
import org.openqa.selenium.Alert;
public class SpecialCases {
public void handleAlert(WebDriver driver) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
// Wait for the alert to appear
wait.until(ExpectedConditions.alertIsPresent());
Alert alert = driver.switchTo().alert();
System.out.println("Alert text: " + alert.getText());
// Click OK (accept) or Cancel (dismiss)
alert.accept(); // click OK
// alert.dismiss(); // click Cancel
// Alert with input (prompt)
alert.sendKeys("my input");
alert.accept();
}
public void handleFrame(WebDriver driver) {
// Switch to an iframe by index, name/id, or element
driver.switchTo().frame(0);
driver.switchTo().frame("frame-name");
driver.switchTo().frame(driver.findElement(By.id("content-iframe")));
// Perform actions inside the iframe
driver.findElement(By.id("button-inside-iframe")).click();
// Return to the main page
driver.switchTo().defaultContent();
// If frames are nested — return to the parent frame
driver.switchTo().parentFrame();
}
public void handleNewWindow(WebDriver driver) {
String originalWindow = driver.getWindowHandle();
// Click a link that opens a new tab/window
driver.findElement(By.linkText("Open in a new tab")).click();
// Switch to the new window
for (String window : driver.getWindowHandles()) {
if (!window.equals(originalWindow)) {
driver.switchTo().window(window);
break;
}
}
// Perform actions in the new window
System.out.println("New window URL: " + driver.getCurrentUrl());
// Close the new window and return to the original
driver.close();
driver.switchTo().window(originalWindow);
}
public void handleCookies(WebDriver driver) {
// Get all cookies
driver.manage().getCookies().forEach(c ->
System.out.println(c.getName() + "=" + c.getValue()));
// Add a cookie (e.g. to skip login)
org.openqa.selenium.Cookie sessionCookie = new org.openqa.selenium.Cookie(
"session_token", "abc123xyz",
"example.com", "/", null
);
driver.manage().addCookie(sessionCookie);
// Delete all cookies (to reset state)
driver.manage().deleteAllCookies();
}
}
When to Use Selenium and When Not To #
USE SELENIUM WHEN:
✓ End-to-end tests — verifying complete flows from UI to database
✓ Regression tests — making sure old features aren't broken after changes
✓ Web scraping — pages that need JavaScript to render content
✓ Cross-browser testing — verifying on Chrome, Firefox, Edge, Safari
✓ Complex form tests with validation, file uploads, dynamic dropdowns
✓ Interactions that need a real browser (WebRTC, canvas, WebGL)
CONSIDER ALTERNATIVES WHEN:
✗ Pure unit or integration tests → no browser needed, use JUnit + Mockito
✗ API testing → REST Assured or Postman are more targeted
✗ Performance/load testing → JMeter or k6 fit better
✗ Pages that can be parsed without JavaScript → Jsoup is far faster for scraping
✗ Very fast, numerous tests → Selenium is slow; consider Playwright or Cypress
✗ Native mobile tests → Appium (Selenium-based) for Android/iOS
Summary #
- Selenium Manager (built into Selenium 4) automatically manages drivers — no more downloading
chromedriveror using a third-party WebDriverManager.- Always close the driver with
quit()in@AfterEach—quit()closes all windows and kills the driver process, whileclose()only closes the active tab.- Locator priority: ID > data-testid > CSS selector > relative XPath. Avoid absolute XPath and framework-auto-generated classes — both are very fragile.
- Explicit waits (
WebDriverWait) are mandatory, notThread.sleep(). Wait for specific conditions (elementToBeClickable,visibilityOf,urlContains) instead of inaccurate fixed times.- The Page Object Model is the mandatory pattern for maintainable tests — one class per page, one method per action, locators not scattered across test classes.
- The
ActionsAPI for complex interactions: hover, double click, drag and drop, key combinations, and scrolling.JavascriptExecutoras a last resort for actions that can’t be done via WebDriver.- Take automatic screenshots when tests fail using the JUnit 5
TestWatcher— very helpful for debugging without manually reproducing failures.- Selenium fits best for end-to-end tests and JavaScript-dependent web scraping — for unit and API tests, use lighter, more targeted tools.