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

Build it as a normal Java project managed by Maven or Gradle, add Selenium Java and TestNG, create a WebDriver lifecycle around TestNG annotations, select tests in testng.xml, and run the suite through your build tool. Selenium WebDriver controls a browser; TestNG supplies test execution, assertions, grouping, parallel scheduling, and reporting. Keeping those responsibilities separate makes the program easier to run locally and in CI.

What the program contains

A maintainable Selenium TestNG program has five parts:

  • A Maven or Gradle build that resolves Selenium and TestNG dependencies.
  • A test class whose setup creates a WebDriver, whose @Test methods exercise behavior, and whose teardown always quits the browser.
  • Assertions that decide whether the behavior passed.
  • A TestNG suite definition selecting classes, methods, groups, or packages.
  • A build-tool command that runs the same suite locally and in continuous integration.

WebDriver is the browser-control layer. It does not determine pass or fail; TestNG runs the methods and evaluates their assertions.

Create the Java project

Maven layout

Create this directory structure:

selenium-testng-demo/
├── pom.xml
├── testng.xml
└── src/
    └── test/
        └── java/
            └── example/
                └── SearchTest.java

Put test sources under src/test/java. Keep testng.xml at the project root (or under src/test/resources if your build points to that path).

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

pom.xml

The dependency coordinates are Selenium’s Java binding and TestNG. Pin the versions your team has approved; do not leave a production build on an unreviewed moving version.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>selenium-testng-demo</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <selenium.version>SET_APPROVED_SELENIUM_VERSION</selenium.version>
    <testng.version>SET_APPROVED_TESTNG_VERSION</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>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.2</version>
        <configuration>
          <suiteXmlFiles>
            <suiteXmlFile>testng.xml</suiteXmlFile>
          </suiteXmlFiles>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Replace the two version properties with the releases selected by your organization. Committing those values makes a local run and a CI run resolve the same libraries.

Gradle alternative

Gradle’s TestNG runner is equivalent. In build.gradle, add the two dependencies with pinned versions and configure the test task:

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.seleniumhq.selenium:selenium-java:PINNED_VERSION'
    testImplementation 'org.testng:testng:PINNED_VERSION'
}

test {
    useTestNG() {
        suites 'testng.xml'
    }
}

Use one build system per repository unless there is a clear reason to maintain both.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Write a WebDriver test with TestNG

This example uses TestNG’s per-method lifecycle, so every test starts with a fresh browser and ends by quitting it.

package example;

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class SearchTest {
    private WebDriver driver;

    @BeforeMethod
    public void setUp() {
        driver = new ChromeDriver();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        driver.manage().window().maximize();
    }

    @Test(groups = {"smoke"})
    public void pageTitleContainsExample() {
        driver.get("https://example.com/");
        String heading = driver.findElement(By.cssSelector("h1")).getText();
        Assert.assertEquals(heading, "Example Domain");
    }

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

Why each annotation is there

  • @BeforeMethod runs before each test method. Use @BeforeClass when a class-level fixture is genuinely safe to share.
  • @Test marks executable behavior. Put assertions in the test, not in page-object constructors.
  • @AfterMethod(alwaysRun = true) runs even after a failure, preventing orphaned browser processes.
  • quit() closes every window and ends the driver session; close() only closes the current window.

Prefer explicit waits for dynamic elements over a large implicit wait. For example:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement button = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("submit")));
button.click();

Configure testng.xml

A suite can contain one or more <test> elements, and each test can include classes, methods, groups, or packages. This file runs the example class and passes a browser parameter:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Web smoke suite" verbose="1">
  <parameter name="browser" value="chrome"/>
  <test name="Smoke tests">
    <groups>
      <run>
        <include name="smoke"/>
      </run>
    </groups>
    <classes>
      <class name="example.SearchTest"/>
    </classes>
  </test>
</suite>

If you use the parameter, receive it with @Parameters("browser") in setup and create the matching driver. Otherwise remove the parameter from the XML. Keep the suite file in source control so developers and CI select the same tests.

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

Other selection patterns

  • Methods: add <methods><include name="pageTitleContainsExample"/></methods> inside a class to run only named methods.
  • Groups: include or exclude groups such as smoke, regression, or api.
  • Packages: use <packages><package name="example"/></packages> for a package-wide selection.

Run the suite

  1. Install a supported JDK and put its java executable on PATH.
  2. From the directory containing pom.xml, run mvn test. Surefire reads testng.xml and writes results under target/surefire-reports.
  3. To run one suite explicitly, use mvn -Dsurefire.suiteXmlFiles=testng.xml test.
  4. With Gradle, run ./gradlew test; reports are under build/reports/tests/test.

Run a single Maven class with mvn -Dtest=example.SearchTest test, but remember that a class filter can bypass the groups and parameters you defined in the suite file.

Do you need to install ChromeDriver manually?

Usually, no. Selenium Manager can discover, download, and cache the required driver and, where supported, the browser. Its documented cache location is ~/.cache/selenium. Starting new ChromeDriver() allows Selenium to invoke that management automatically.

Manual management is still reasonable when a locked-down CI image cannot download binaries, when your organization mirrors drivers internally, or when browser versions must be frozen exactly. In those cases, install the driver on the image, put it on PATH, or configure the appropriate driver service. Check the browser/driver pair whenever a session fails to start; a mismatch commonly produces a session-creation error.

Run tests in parallel with TestNG

Parallelism shortens wall-clock time but is safe only when each concurrent test owns its browser and its test data. Do not store a mutable WebDriver in a static field shared by threads.

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

Parallel classes

Change the suite declaration:

<suite name="Parallel suite" parallel="classes" thread-count="3">
  <test name="UI tests">
    <classes>
      <class name="example.SearchTest"/>
      <class name="example.CheckoutTest"/>
      <class name="example.ProfileTest"/>
    </classes>
  </test>
</suite>

TestNG also supports parallel="methods", parallel="tests", and parallel="instances". A parallel data provider can feed multiple data sets concurrently. Choose the narrowest mode that matches your isolation model.

Thread-local driver pattern

If a framework layer must expose a driver across helper classes, use a ThreadLocal<WebDriver>, initialize it before each test, and remove it after quitting. Even then, isolate accounts, files, records, and other server-side data; thread-local browsers do not prevent two tests from editing the same account.

Move to Selenium Grid

Grid is the next step when browsers should run on another machine, across operating systems, or concurrently on dedicated nodes. Start a Selenium Server in standalone mode, then point the test at its remote endpoint:

java -jar selenium-server.jar standalone
WebDriver driver = new RemoteWebDriver(
    URI.create("http://localhost:4444").toURL(),
    new ChromeOptions());

Keep the same TestNG lifecycle; only driver construction changes. For CI, record the server version, browser version, node capacity, screen size, and artifacts (logs, screenshots, and page source) so a remote failure can be reproduced.

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

Reliability, speed, and cost decisions

Make failures diagnosable

  • Capture a screenshot and page source in an @AfterMethod listener when a test fails.
  • Use stable IDs or accessible attributes instead of brittle XPath tied to layout.
  • Wait for a specific state (visible, enabled, URL, or text) rather than sleeping for a fixed number of seconds.
  • Keep test data disposable and resettable; never depend on execution order.

Control runtime

  • Use headless mode only when the application behaves identically without a visible display; keep a headed option for debugging.
  • Run a small smoke group on every change and the larger regression group on a scheduled or gated job.
  • Increase TestNG’s thread count only after measuring node capacity, application rate limits, and test-data contention. There is no universal safe number.

Budget infrastructure

Local execution costs developer CPU and memory. Grid adds server and browser-node administration, but gives you remote environments and concurrency. Selenium and TestNG themselves do not provide a hosted browser fleet; that infrastructure is a separate operational decision.

Or skip the browser setup

If your task is to obtain a clean page image or PDF rather than interactively test behavior, ScreenshotNeo is the first screenshot API to try: it removes consent banners, popups, and chat widgets before capture, and bills only clean shots.

One request is enough (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 can also capture full pages with lazy images, a CSS-selected element, dark mode and device presets; apply custom CSS or JavaScript; click or hide elements; wait for a selector, delay, or network idle; block ads, trackers, requests, or resource types; set headers, cookies, user agent, authorization, timezone, and geolocation; produce PDFs; resize images; cache with a chosen TTL; create signed links; submit asynchronous jobs with signed webhooks; capture up to 100 URLs per bulk call; and expose usage and OpenAPI endpoints. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

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

Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

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

Troubleshooting

“Unable to create a new session”

Check that the browser is installed, the process can download or locate a driver, and the browser/driver versions are compatible. In restricted CI, preinstall and pin both.

The test hangs at an element lookup

Replace an implicit wait or sleep with an explicit wait for the element’s actual condition. Confirm the locator targets the current frame and that the page has finished navigation.

Tests pass alone but fail in parallel

Look for shared static drivers, shared accounts, fixed filenames, ports, or database rows. Create those resources per test or protect them with controlled fixtures; reducing thread-count is a diagnostic, not a complete fix.

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

Maven runs no tests

Verify the suite path in Surefire, the class name in testng.xml, and the conventional src/test/java location. Run Maven with the class filter to distinguish discovery problems from suite-selection problems.

Grid reports connection refused

Confirm the Selenium Server process is running, port 4444 is reachable from the test machine, and the URL includes the correct scheme and path. A local browser driver cannot compensate for an unavailable remote server.

FAQ

Can TestNG replace Selenium?

No. TestNG schedules and evaluates tests; Selenium WebDriver drives the browser. A UI test normally needs both.

Is testng.xml mandatory?

No. Maven, Gradle, annotations, and build-tool filters can select tests without it. A suite file is valuable when you need a repeatable collection, groups, parameters, or parallel settings.

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

Should every test create a new browser?

For isolation, a new driver per test method is the safest default. Share a session only when the scenario intentionally models a sequence and the resulting coupling is acceptable.

When should a team use Grid?

Use Grid when local execution cannot provide the required browser/OS coverage, remote execution, or concurrency. Start locally, then move the same WebDriver code to RemoteWebDriver as infrastructure becomes available.

Frequently Asked Questions

Can TestNG replace Selenium?

No. TestNG schedules and evaluates tests; Selenium WebDriver drives the browser. A UI test normally needs both.

Is testng.xml mandatory?

No. Maven, Gradle, annotations, and build-tool filters can select tests without it. A suite file is useful for repeatable groups, parameters, and parallel settings.

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

Should every test create a new browser?

A new driver per test method is the safest isolation default. Share a session only for an intentionally sequential scenario.

When should a team use Grid?

Use Grid when local execution cannot provide the required browser/OS coverage, remote execution, or concurrency.

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.