Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Set up Selenium Java and TestNG in your build, create one isolated WebDriver per test method, synchronize with explicit waits, and let Maven Surefire discover or run your TestNG suite. The pattern below works for a maintainable Java test project; replace the example URL, selectors, credentials, browser, and driver setup with those from your application.

What you need before writing a test

  • A supported JDK. TestNG’s official examples use TestNG 7.5.1 for JDK 8 and 7.9.0 for JDK 11. Treat those as examples and verify the version that matches your project.
  • Maven or Gradle.
  • A Selenium-compatible browser and driver available to the test process. Keep browser, driver, and Selenium Java versions compatible.
  • A test environment whose URL, test account, and selectors are stable enough for automation.

Pin the dependency versions in source control. There is no single universal Selenium/TestNG version matrix, so check the current Selenium Java release and your JDK support policy before choosing versions.

Add Selenium and TestNG to the build

Maven

Put this in pom.xml. The Selenium version is deliberately a project property: set it to the current Selenium Java release you have selected, rather than copying an unqualified version number.

<properties>
  <maven.compiler.source>11</maven.compiler.source>
  <maven.compiler.target>11</maven.compiler.target>
  <selenium.version>your-selected-selenium-version</selenium.version>
  <testng.version>7.9.0</testng.version>
</properties>

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

If your project remains on JDK 8, use a TestNG release compatible with that JDK, such as the 7.5.1 example documented by TestNG. Do not assume that the JDK 11 example is valid for JDK 8.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Gradle

The equivalent Gradle declaration is:

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.seleniumhq.selenium:selenium-java:<selected Selenium version>")
    testImplementation("org.testng:testng:7.9.0")
}

test {
    useTestNG()
}

Replace the marked Selenium value with the current release chosen for your project and commit that value. Gradle’s useTestNG() tells the test task to use TestNG instead of its default engine.

Create a TestNG class with a WebDriver fixture

A TestNG test class is a Java class containing at least one TestNG annotation. Use @BeforeMethod and @AfterMethod when each test should receive a fresh browser. That prevents cookies, local storage, navigation history, and modified application state from leaking between methods.

package com.example.ui;

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class LoginTest {
    private WebDriver driver;
    private WebDriverWait wait;

    @BeforeMethod
    public void setUp() {
        // Make a compatible ChromeDriver available to this process.
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @Test
    public void userCanLogIn() {
        driver.get("https://example.test/login");

        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username")))
            .sendKeys("user");
        driver.findElement(By.id("password")).sendKeys("password");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        String heading = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("h1")))
                            .getText();
        Assert.assertEquals(heading, "Dashboard");
    }

    @AfterMethod
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

The URL, credentials, selectors, and expected heading are illustrative. Use a secret-management mechanism rather than committing real credentials. The null check matters: if browser startup fails in @BeforeMethod, teardown must not throw a second exception while trying to close an uninitialized driver.

Choose the correct lifecycle scope

Annotation Use it for Typical WebDriver implication
@BeforeMethod / @AfterMethod Per-test setup and cleanup Safest isolation; create and quit one driver for every test method.
@BeforeClass / @AfterClass One fixture for all methods in a class Faster startup, but state can leak unless every test resets it.
@BeforeTest / @AfterTest A TestNG <test> section Use only when several classes intentionally share a broader fixture.
@BeforeSuite / @AfterSuite The complete suite Appropriate for suite-wide services, not normally a shared mutable browser.

TestNG also provides @BeforeGroups and @AfterGroups for group-specific setup and cleanup.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Synchronize dynamic pages with explicit waits

driver.get() waits for a page-load state, but JavaScript can continue changing the DOM afterward. An explicit wait polls for a condition and times out if that condition never becomes true:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("save"))).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector(".status"), "Saved"));

This is preferable to fixed sleeps because the test proceeds as soon as the application is ready and reports a timeout at the condition that failed. Selenium’s Java WebDriverWait API uses a Duration and ignores NotFoundException while polling by default.

Implicit, explicit, and fluent waits

  • Implicit wait: a global element lookup delay. It is simple but hides timing policy and can make failures harder to diagnose.
  • Explicit wait: a targeted condition such as visibility, clickability, URL, title, or text. Prefer this for most dynamic interactions.
  • Fluent wait: an explicit wait with customized polling and ignored exceptions when a particular application needs it.

Do not use arbitrary sleeps as the primary synchronization strategy. Wait for the state the user or test actually needs.

Use TestNG annotations and assertions effectively

@Test can annotate a method or class. Its API supports groups, dependencies, data providers, expected exceptions, invocation counts, and enabled flags.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

@Test(groups = "smoke")
public class SearchTest {
    @DataProvider(name = "queries")
    public Object[][] queries() {
        return new Object[][] {{"selenium"}, {"testng"}};
    }

    @Test(dataProvider = "queries")
    public void searchReturnsResults(String query) {
        // Navigate with WebDriver, wait for results, and assert the rendered state.
    }
}

Groups let you select subsets such as smoke or regression. Data providers reuse the same test logic across input sets. Listeners and reporters can add custom reporting or integration hooks; introduce them after the basic lifecycle is reliable.

Run the tests with Maven Surefire

Convention-based discovery

Maven Surefire conventionally discovers classes named *Test.java, *Tests.java, or similar patterns. Run:

mvn test

Keep test classes under src/test/java. A class named LoginTest with a method annotated @Test should be discovered without a suite file.

Run an explicit TestNG suite

Create src/test/resources/testng.xml:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="UI suite">
  <test name="login">
    <classes>
      <class name="com.example.ui.LoginTest"/>
    </classes>
  </test>
</suite>

Configure Surefire to use it. Surefire 3.6.0 documentation describes TestNG integration and suite, group, parameter, and parallel configuration; keep the plugin version pinned with the rest of your build.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.6.0</version>
      <configuration>
        <suiteXmlFiles>
          <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
        </suiteXmlFiles>
      </configuration>
    </plugin>
  </plugins>
</build>

With that configuration, mvn test runs the classes selected by the XML file. Use suite XML when you need a named collection, ordering, groups, parameters, or parallel settings; convention discovery is simpler for a small project.

Parameters, groups, and parallel execution

TestNG parameters let the same suite run against different environments or browser settings. Define them in the suite and accept them in a configuration method:

<suite name="environments">
  <parameter name="baseUrl" value="https://staging.example.test"/>
  <test name="smoke">
    <classes>
      <class name="com.example.ui.LoginTest"/>
    </classes>
  </test>
</suite>
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;

@Parameters("baseUrl")
@BeforeMethod
public void setUp(String baseUrl) {
    driver = new ChromeDriver();
    wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    driver.get(baseUrl);
}

Only add parallel execution after driver isolation is designed. WebDriver is stateful; sharing one instance between concurrently running tests causes navigation, cookies, and element operations to interfere. Give each parallel worker its own driver and avoid mutable static test state. Start with sequential execution, then configure TestNG’s parallel mode and thread count in suite XML or Surefire once reports are understandable.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common failures and fixes

Driver cannot be created

Symptom: a driver or session-creation exception in @BeforeMethod. Fix: verify that the browser is installed, the driver is on the process path or otherwise configured, and the browser-driver-Selenium versions are compatible. The null-safe teardown prevents a second failure from obscuring the original one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Element not found or not clickable

Symptom: NoSuchElementException, ElementNotInteractableException, or an explicit-wait timeout. Fix: confirm the selector against the current DOM, wait for the required state, switch to the correct frame when applicable, and ensure the element is not covered by a modal or consent layer.

Intermittent timeout

Symptom: the same test passes locally but sometimes times out in CI. Fix: replace sleeps with a condition-based wait, wait for network-driven content by its visible result, capture the failing URL and page state in test output, and use a timeout appropriate for the CI environment without making every wait unnecessarily long.

Tests pass alone but fail as a suite

Symptom: order-dependent failures or authentication leakage. Fix: restore state in @AfterMethod, use a fresh driver per method, remove static mutable fixtures, and check whether parallel execution was enabled before isolation was complete.

Maven reports no tests

Symptom: mvn test completes without running the class. Fix: place the class under src/test/java, use a conventional test name, ensure the method or class has @Test, and verify that Surefire’s suite XML or include patterns point to the intended class.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than an interactive assertion, ScreenshotNeo is a direct HTTP alternative to maintaining browser-capture code. Its API accepts a URL and returns PNG, JPEG, WebP, or PDF; before capture it accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Each cleanup step can be disabled.

One call with cURL:

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

See the ScreenshotNeo API documentation for all parameters. The same request in Python:

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

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}`);

ScreenshotNeo bills only clean shots. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Why does the null check in tearDown matter?

If browser creation fails in @BeforeMethod, the driver field remains null. Checking it before quit() prevents teardown from throwing a second exception that hides the startup failure.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When should I use suite XML instead of Maven’s default discovery?

Use suite XML when you need named suites, groups, parameters, class selection, or parallel settings. Convention-based discovery is sufficient for a small set of classes named with Maven’s test patterns.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.