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| JAVA

Selenium 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 chromedriver or using a third-party WebDriverManager.
  • Always close the driver with quit() in @AfterEachquit() closes all windows and kills the driver process, while close() 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, not Thread.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 Actions API for complex interactions: hover, double click, drag and drop, key combinations, and scrolling. JavascriptExecutor as 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.

← Previous: Hibernate   Next: Articles & Resources →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact