ScreenshotNeo

BlogHow-to

How to Use TestNG with Selenium

Set up Selenium WebDriver with TestNG in a Java Maven project, write isolated browser tests, configure suites, and run them reliably.

By the ScreenshotNeo team4 October 202610 min read

TestNG organizes and runs Java tests; Selenium WebDriver controls the browser. Use them together by adding Selenium Java and TestNG to a Maven project, creating a WebDriver for each test (or each deliberately chosen lifecycle scope), and writing TestNG @Test methods that assert observable page behavior. Use a TestNG suite XML file or Maven Surefire to select and run tests.

The example below uses Java, Maven, Selenium WebDriver, and TestNG. Dependency versions change over time, so select versions compatible with your Java runtime from the projects’ official documentation rather than copying an old version number. Selenium’s setup requires the language binding, a browser, and its driver; Selenium Manager can resolve drivers in supported Selenium releases, or you can configure a driver explicitly. See the official Selenium getting-started guidance and Java library installation.

1. Create a Maven project and add dependencies

Use a standard Maven layout, with test classes under src/test/java. Put Selenium and TestNG in test scope because they are only needed to compile and run tests. Replace the version placeholders with compatible releases from the Selenium Java installation guide and TestNG Maven guide.

<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-example</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <selenium.version>REPLACE_WITH_COMPATIBLE_VERSION</selenium.version>
    <testng.version>REPLACE_WITH_COMPATIBLE_VERSION</testng.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </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>REPLACE_WITH_COMPATIBLE_VERSION</version>
      </plugin>
    </plugins>
  </build>
</project>

Replace maven.compiler.release if your project targets a different supported Java version. The TestNG Maven guide’s visible examples may be specific to particular JDK versions; verify them against your project’s Java requirements.

2. Write a browser test with setup and cleanup

This example opens a public page, checks its title, and always closes the browser. The page and assertion are illustrative; replace them with a stable application URL and behavior that matters to your project.

package example;

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 HomePageTest {
    private WebDriver driver;

    @BeforeMethod
    public void startBrowser() {
        // With supported Selenium versions, Selenium Manager can resolve
        // a compatible driver when Chrome is installed and available.
        driver = new ChromeDriver();
    }

    @Test
    public void homePageHasExpectedTitle() {
        driver.get("https://example.com");
        Assert.assertTrue(
            driver.getTitle().contains("Example Domain"),
            "Expected the page title to contain Example Domain"
        );
    }

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

TestNG describes a test method as a Java method annotated with @Test. Here @BeforeMethod creates a fresh browser before each test and @AfterMethod(alwaysRun = true) cleans it up even when a test fails. quit() closes the whole WebDriver session; close() closes only the current window and can leave the session process running.

3. Choose the right test lifecycle

Lifecycle scope is a tradeoff between startup time and isolation. A fresh browser per method is a useful default while building a suite because cookies, navigation, and browser state do not leak between tests.

Scope Typical annotations Use when Tradeoff
Each test method @BeforeMethod, @AfterMethod Tests should be independent and easy to rerun. More browser startups.
Each test class @BeforeClass, @AfterClass Methods intentionally share a session or setup is expensive. State can leak between methods; ordering assumptions become risky.
Each suite @BeforeSuite, @AfterSuite Managing resources shared across a whole suite. A shared browser session is usually unsafe for independent tests.

Do not share one mutable WebDriver between parallel tests. If a browser session is reused, make the shared state and test ordering intentional, and account for failures that leave the session in an unexpected state.

4. Use explicit waits for dynamic pages

Modern pages often render after navigation returns. Avoid fixed sleeps as the normal synchronization strategy: they waste time when a page is fast and can still be too short when it is slow. Wait for the condition the test needs.

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

@Test
public void searchShowsResults() {
    driver.get("https://example.com/search");
    driver.findElement(By.name("q")).sendKeys("selenium");
    driver.findElement(By.cssSelector("button[type='submit']")).click();

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    var heading = wait.until(
        ExpectedConditions.visibilityOfElementLocated(By.cssSelector("h1.results"))
    );
    Assert.assertTrue(heading.getText().contains("Results"));
}

Pick locators that express the page’s stable structure: an ID, accessible name, or purposeful data attribute is generally easier to maintain than a long positional CSS path. If the app exposes a stable test identifier, use it consistently. Set timeouts to match expected application behavior; longer waits can hide slow pages and make failures slower to diagnose.

5. Select tests with a TestNG suite

A testng.xml file can select classes, methods, groups, and execution settings. For example, place this file at the project root to run the example class:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Browser suite">
  <test name="Smoke tests">
    <classes>
      <class name="example.HomePageTest"/>
    </classes>
  </test>
</suite>

Run through Maven Surefire with mvn test; Surefire supports TestNG discovery and execution. To run a particular suite file, configure the Surefire plugin to include it as a suite XML file, following the Surefire TestNG documentation. You can also run a suite directly through TestNG’s supported command-line entry point; the exact classpath depends on how your dependencies are assembled.

Groups and selected methods

Tag related tests with groups, then include or exclude them in suite XML. This lets a project separate, for example, quick smoke checks from slower integration coverage without duplicating test classes.

@Test(groups = {"smoke"})
public void homePageHasExpectedTitle() {
    // test body
}
<suite name="Smoke suite">
  <test name="Smoke">
    <groups>
      <run>
        <include name="smoke"/>
      </run>
    </groups>
    <classes>
      <class name="example.HomePageTest"/>
    </classes>
  </test>
</suite>

For method-level selection, list the class and the methods in its <methods> element. Keep suite selection in version control so local and CI runs use the same intent.

6. Run tests in parallel only when they are isolated

TestNG supports parallel execution by methods, classes, <test> blocks, or instances. Pick the unit that matches your test design. For example, parallelizing classes allows separate classes to run concurrently, but each test still needs a separate driver session and non-conflicting test data.

<suite name="Parallel browser suite" parallel="classes" thread-count="3">
  <test name="UI tests">
    <classes>
      <class name="example.HomePageTest"/>
      <class name="example.AccountPageTest"/>
      <class name="example.SearchPageTest"/>
    </classes>
  </test>
</suite>

Before increasing the thread count, check that each concurrent test gets its own WebDriver, that test accounts and records do not collide, and that any shared helpers are thread-safe. Parallelism can reduce elapsed time when work is independent, but it also raises simultaneous browser and machine resource use. Start with a small thread count and inspect failures for race conditions before scaling.

7. Capture screenshots when a test fails

A local Selenium screenshot can preserve the browser state at failure time. Use a listener or a shared failure hook appropriate to your TestNG setup, and write files to a CI artifact directory with a unique name per test and run. Do not use one fixed filename when tests run in parallel.

import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;

static void saveScreenshot(WebDriver driver, Path destination) throws Exception {
    byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
    Files.createDirectories(destination.getParent());
    Files.write(destination, png);
}

The example helper only captures the current viewport. A full-page image is browser-specific and is not guaranteed by this WebDriver call. Protect screenshots and logs if pages can show personal, account, or other sensitive data.

Or skip the browser setup

If you need a page screenshot for documentation, visual review, or an agent workflow rather than an interactive Selenium test, ScreenshotNeo provides a website screenshot API and MCP server. The API accepts a URL and returns an image or PDF. Its clean-shot flow accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status.

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

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

See the ScreenshotNeo API documentation for request options. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.

8. Troubleshoot common failures

Symptom Likely cause Fix
Cannot resolve symbol for TestNG or Selenium Dependency missing, wrong scope, or Maven project not reloaded. Check both dependencies in pom.xml, confirm test sources are under src/test/java, then reload Maven.
TestNG tests are not discovered by mvn test Surefire configuration or test naming/discovery does not match the project setup. Check the Surefire TestNG guide, confirm the provider and suite configuration, and inspect Maven’s test output.
SessionNotCreatedException Browser and driver mismatch, unsupported browser installation, or browser startup failure. Confirm the browser is installed and compatible. Use Selenium Manager in a supported Selenium release or provide a matching driver configuration.
Unable to locate element Wrong locator, page has not rendered, or the element is inside a frame or shadow root. Verify the locator against the current page, wait for the required condition, and switch into the correct frame or use the appropriate shadow-root API.
Element is not interactable or click is intercepted Element is hidden, covered by an overlay, outside the viewport, or not ready. Wait for visibility/clickability, handle the overlay, and scroll or use a user-visible interaction. Avoid JavaScript clicks that bypass the behavior the test is meant to verify.
Tests pass alone but fail in a suite Shared browser state, test data collisions, ordering assumptions, or parallel access to mutable state. Use fresh sessions, unique test data, remove ordering dependencies, and temporarily disable parallel execution to identify shared-state problems.
Browser processes remain after failures Cleanup hook did not run or cleanup used close() rather than ending the session. Use @AfterMethod(alwaysRun = true) and call quit() in a null-safe cleanup method.
Tests are slow or flaky around navigation Fixed sleeps, overloaded CI resources, or a wait condition unrelated to the next action. Wait for a specific page condition, check resource pressure, and tune timeouts to observed application behavior.

9. Performance, reliability, and cost

  • Startup cost: A new browser per method is easier to isolate but takes more time. Keep that scope until measured suite duration justifies reuse.
  • Wait strategy: Explicit, condition-based waits avoid unnecessary fixed delays and make failures more specific.
  • Parallel load: More workers consume more CPU and memory and may overload the application under test. Increase concurrency gradually and ensure data isolation.
  • Repeatability: Pin compatible dependency versions in the build, use stable locators, control test data, and record browser/driver setup in CI configuration.
  • Failure evidence: Save screenshots and useful logs on failure, use unique artifact names, and restrict access to artifacts that may contain sensitive page content.
  • Cost: Selenium and TestNG are libraries, but browser execution consumes local or CI machine capacity. Account for CI minutes, browser workers, and any separately operated grid. ScreenshotNeo’s optional API pricing is $0 for 1,000 shots monthly, then Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is on every plan.

Frequently asked questions

Is TestNG a replacement for Selenium?

No. Selenium controls the browser; TestNG supplies Java test structure, lifecycle, selection, and execution.

Can I use TestNG with a browser other than Chrome?

Yes. Use the corresponding Selenium WebDriver implementation and have the matching browser available. Driver setup depends on the browser and environment.

Should I use JUnit or TestNG?

This guide covers TestNG. Choose based on the annotations, suite features, integrations, and conventions your project needs; avoid mixing runners in one suite without a clear reason.

Can Selenium take a screenshot without TestNG?

Yes. Selenium’s screenshot API is separate from the test framework; TestNG can determine when to invoke it, such as after a failure.

References