ScreenshotNeo

BlogHow-to

How to Write End-to-End Tests with Selenium 4 and Java

Build a maintainable Selenium 4 end-to-end test in Java: add dependencies, run a real workflow, wait for the right state, assert the result, and clean up.

By the ScreenshotNeo team4 October 202610 min read

A Selenium 4 end-to-end test drives a real browser through a user workflow and checks the visible result. Add Selenium to a Java project, use JUnit or TestNG to run and report the test, wait for a specific browser condition instead of guessing with sleeps, and always call quit() to close the session. Selenium Manager can resolve browser drivers automatically in supported Selenium versions, so a separate ChromeDriver download is often unnecessary.

This guide builds a JUnit 5 test that submits a form and checks its confirmation. The example target is https://example.test/form, a placeholder: replace it and the locators with an application page and controls that exist in your test environment. The code is instructional and has not been represented as executed.

1. Set up a Java project

Selenium’s Java bindings are published as org.seleniumhq.selenium:selenium-java. Add it as a test dependency, choose a Selenium release compatible with your Java runtime and browser environment, and pin the version through your normal dependency-management process. Consult Selenium’s installation guide for current Maven and Gradle coordinates.

Maven

<properties>
  <maven.compiler.release>17</maven.compiler.release>
  <selenium.version>4.XX.X</selenium.version>
  <junit.version>5.XX.X</junit.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>${selenium.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.5.2</version>
      <configuration>
        <useModulePath>false</useModulePath>
      </configuration>
    </plugin>
  </plugins>
</build>

Replace the illustrative 4.XX.X and 5.XX.X values with actual releases approved for your project. Version numbers change; verify current releases and Java compatibility before committing.

Gradle

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

def seleniumVersion = '4.XX.X'
def junitVersion = '5.XX.X'

dependencies {
    testImplementation "org.seleniumhq.selenium:selenium-java:${seleniumVersion}"
    testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
}

test {
    useJUnitPlatform()
}

Keep versions centralized and pinned. JUnit and TestNG are both used with Selenium; pick the runner that fits your team’s fixture, reporting, parameterization, and parallel-run needs. Selenium provides runner guidance, while noting that its organizing page is incomplete: Organizing and Executing Selenium Code.

2. Write a complete JUnit test

Put this class under src/test/java. It starts Chrome, navigates to the test page, fills and submits the form, waits until the confirmation is visible, checks its text, and quits Chrome even if an assertion or browser action fails.

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

class FormSubmissionTest {
    @Test
    void submitsFormAndShowsConfirmation() {
        WebDriver driver = new ChromeDriver();
        try {
            driver.get("https://example.test/form");

            driver.findElement(By.name("email"))
                  .sendKeys("reader@example.test");
            driver.findElement(By.cssSelector("button[type='submit']"))
                  .click();

            WebDriverWait wait = new WebDriverWait(
                driver, Duration.ofSeconds(10));
            WebElement confirmation = wait.until(
                ExpectedConditions.visibilityOfElementLocated(
                    By.id("confirmation")));

            assertEquals("Submitted", confirmation.getText().trim());
        } finally {
            driver.quit();
        }
    }
}

Run it with mvn test for Maven or ./gradlew test for Gradle. A passing test depends on a reachable test application with the specified email field, submit button, and confirmation element. Keep test data isolated and use a test environment rather than submitting data to a production workflow.

3. Choose locators that describe the application

A locator identifies the element Selenium should find. Use stable attributes that belong to the application’s intended behavior, such as an ID, name, accessible role or label when supported by the page, or a focused CSS selector. Avoid selectors tied to incidental layout or generated class names because ordinary UI changes can break them.

Locator Example Good fit
ID By.id("confirmation") A unique, stable element ID.
Name By.name("email") Form controls with a meaningful name.
CSS selector By.cssSelector("button[type='submit']") A concise selector based on stable attributes.
Link text By.linkText("Account") A link whose visible text is stable and unambiguous.

Prefer one locator that identifies the intended control over a long chain through parent and child layout elements. If a page has multiple matching controls, narrow the selector or scope the lookup to a meaningful container.

4. Wait for the state the next step needs

Pages update asynchronously. A click may trigger a network request, a transition, or client-side rendering, so an immediate lookup can race the application. Use an explicit wait for the state the test needs: visibility before reading text, clickability before clicking, or presence when visibility is not necessary.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement ready = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("continue")));
ready.click();

WebElement result = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("result")));
String message = result.getText();

WebDriverWait is a FluentWait<WebDriver> specialization and its Java constructor takes a Duration. It ignores NotFoundException by default while checking its condition. See the Java API documentation for details.

Common conditions

  • presenceOfElementLocated: the element exists in the DOM; it may not be visible.
  • visibilityOfElementLocated: the element exists and is visible.
  • elementToBeClickable: the element is visible and enabled for interaction.
  • titleIs or urlContains: the browser title or URL reaches an expected value.
  • invisibilityOfElementLocated: a loading overlay or temporary element disappears.

Do not mix implicit and explicit waits casually: combined waiting behavior can make failures harder to reason about. Selenium’s first-script guide describes implicit wait as a placeholder and says it is rarely the best general solution. Avoid fixed sleeps as a synchronization strategy: they delay fast runs and can still be too short for slow ones. See Selenium’s first script guide.

5. Let Selenium Manager handle browser drivers

For ordinary local use, create new ChromeDriver() and let Selenium attempt driver management. Selenium Manager ships with Selenium releases from 4.6 and is used by the bindings when no driver has otherwise been supplied. The project describes automated browser management as available starting in 4.11.0. Behavior depends on Selenium version and environment; consult the Selenium Manager documentation for the release you use.

As Selenium’s documentation puts it, “Thus, the Selenium project has created Selenium Manager, the official driver manager for Selenium, shipped out of the box with every Selenium release.” Selenium Manager can locate, download, and cache drivers; automated browser management is also documented. If your organization pins browser and driver binaries, restricts network access, or uses a specialized setup, supplying a driver or using another manager may be appropriate. Selenium Manager is a fallback path, not a rule that removes manual configuration.

6. Put browser lifecycle in test fixtures as the suite grows

The try/finally example makes cleanup explicit for one test. In a larger JUnit suite, use lifecycle methods so every test gets a predictable session and teardown still runs on failures.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

class BrowserTestBase {
    protected WebDriver driver;

    @BeforeEach
    void startBrowser() {
        driver = new ChromeDriver();
    }

    @AfterEach
    void stopBrowser() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Place actual test methods in this class or extend it according to your project’s conventions. Avoid sharing one mutable browser session across unrelated tests: one test’s cookies, navigation, or state can affect another. Configure parallel execution only after the runner, browser resources, and test data are safe for concurrent use.

7. Run locally first, then consider Selenium Grid

A local browser is the simplest starting point for debugging. Selenium Grid is intended for running tests in parallel across machines and browsers. It adds infrastructure and operational configuration, so introduce it when local execution no longer meets your suite’s needs; there is no universal Grid setup for every CI environment. The Selenium documentation describes the project and Grid role.

Keep tests deterministic before increasing concurrency: each test should own its browser session and data, wait for meaningful conditions, and clean up after itself. Parallel execution can expose shared-state assumptions that are hidden in a serial run.

8. Troubleshoot common failures

Symptom Likely cause What to check or change
SessionNotCreatedException during startup Browser and driver versions are incompatible, browser is missing, or startup is blocked. Check the installed browser, Selenium version, and Selenium Manager logs. Confirm the environment can access the required browser/driver source, or supply a compatible pinned driver.
Driver resolution fails in CI Network restrictions, proxy configuration, permissions, or an unavailable browser binary. Check CI network and proxy settings and browser installation. For controlled environments, provision browser and driver explicitly.
NoSuchElementException Wrong locator, wrong page, late rendering, or an element inside a frame. Verify the current URL and page structure, use a stable locator, wait for the relevant condition, and switch to the correct frame if the control is embedded.
TimeoutException from a wait The condition never became true before the timeout, often because the locator or expected state is wrong. Inspect the page state and locator. Confirm the expected condition matches what the application actually does; increase the timeout only when real latency warrants it.
ElementClickInterceptedException An overlay, animation, or another element covers the target. Wait for the covering element to disappear and for the target to be clickable. Check whether a cookie banner or modal must be handled as part of the user workflow.
StaleElementReferenceException The page replaced or rerendered the element after it was found. Wait for the update, then locate the element again rather than reusing an old WebElement.
Test passes locally but fails intermittently in CI Timing assumptions, shared data, resource contention, or environment differences. Replace sleeps with condition-based waits, isolate data and sessions, and compare browser/runtime configuration. Capture useful failure diagnostics in the runner or CI job.
Browser remains open after a failed assertion Cleanup does not run on all paths. Use finally or the test runner’s teardown hook and call driver.quit().

9. Performance, reliability, and cost

Browser tests are heavier than unit tests because they start and control a browser and may depend on an application and network. Keep the end-to-end layer focused on important user workflows, and cover lower-level logic with faster tests where appropriate. Do not use a shorter timeout to hide a slow or unstable application, and do not use a longer timeout to conceal an incorrect condition.

  • Performance: reuse project setup sensibly, avoid unnecessary navigation and fixed delays, and run independent tests concurrently only when their data and browser sessions are isolated.
  • Reliability: use a stable test environment, deterministic data, semantic locators, explicit waits, and unconditional cleanup. A browser test still depends on the application and browser environment, so diagnose failures rather than automatically retrying every failure.
  • Cost: Selenium itself is a library; operational cost depends on the machines, browsers, CI time, and any Grid or hosted infrastructure your team chooses. The research sources do not provide a universal price or performance benchmark.

Or skip the browser setup

If your goal is to capture a page image or PDF for a visual check, documentation record, or downstream workflow, a screenshot API can avoid managing a browser session for that capture. ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. Its one-call API is not a replacement for Selenium’s interactive end-to-end workflow or test assertions.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

More formats and capture parameters are in the ScreenshotNeo API documentation. The same capture can be requested with Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Or with Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

Replace the sample URL with the page you are authorized to capture. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, with response headers indicating the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.

Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card required.

FAQ

Does Selenium provide the test assertions?

No. Selenium automates the browser. Use JUnit, TestNG, or another test framework to discover tests, make assertions, report results, and manage lifecycle hooks.

Can I use a browser other than Chrome?

Yes. Selenium supports multiple browser drivers. The exact browser installation and driver-management setup depends on the browser and environment you choose.

Should every test be end-to-end?

No. Reserve browser-driven tests for important behavior that needs a real browser and application. Use smaller tests for logic that does not need that full workflow.

When should I move to Grid?

Consider Grid when you need parallel runs across machines or browser types and can support the required infrastructure. Start with local execution while it meets your needs.

Sources