TestNG with Java: The Complete Guide to Supercharge Your Testing Game!

TestNG with Java: The Complete 20‑Page Guide for Selenium Automation ๐Ÿš€

From zero to parallel, data‑driven, flaky‑proof test suites — with jokes, snacks, and sensible defaults


 

1) Read Me First: The Sequence (Your Map) ๐Ÿ—บ️

Follow this order to level up smoothly:
1) Install & verify TestNG
2) Wire it to Selenium
3) Master annotations (setup/teardown),
4) Learn assertions & soft assertions
5) Parameterize with @Parameters & @DataProvider,
6) Add dependencies & groups
7) Run in parallel (safely)
8) Add listeners & retry logic,
9) Generate reports
10) CI & Grid
11) Troubleshoot like a pro.

 We’ll build a mini e‑commerce demo through the chapters (with silly examples — yes, coffee again ☕).


 

2) Setup: Maven, TestNG, Selenium, WebDriverManager

Recommended stack: Maven + Java 11+ + Selenium + TestNG + WebDriverManager. IDE: IntelliJ/Eclipse with TestNG plugin.

Maven dependencies (pom.xml)

<project>
  <dependencies>
    <!-- Test framework -->
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.9.0</version>
      <scope>test</scope>
    </dependency>

    <!-- Selenium -->
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>4.22.0</version>
    </dependency>

    <!-- WebDriverManager for driver binaries -->
    <dependency>
      <groupId>io.github.bonigarcia</groupId>
      <artifactId>webdrivermanager</artifactId>
      <version>5.9.2</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- Run TestNG suites via Surefire -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
        <configuration>
          <suiteXmlFiles>
            <suiteXmlFile>testng.xml</suiteXmlFile>
          </suiteXmlFiles>
          <!-- Pass -D parameters to tests -->
          <systemPropertyVariables>
            <env>${env}</env>
            <browser>${browser}</browser>
          </systemPropertyVariables>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Recommended project layout

.
├─ pom.xml
├─ src
  ├─ main
    └─ java
       └─ com.example.app ... (app/page objects/utils)
  └─ test
     └─ java
        └─ com.example.tests
           ├─ base
             └─ BaseTest.java
           ├─ login
             └─ LoginTests.java
           ├─ product
             └─ ProductTests.java
           └─ listeners
              └─ CustomTestListener.java
└─ testng.xml

Pro‑tip: Keep test data under src/test/resources. Use environments via -Denv=staging and toggle URLs/creds via a Config class.


 

3) TestNG Essentials: What & Why ๐Ÿงฐ

·        Annotations orchestrate setup/teardown like a stage crew.

·        testng.xml is your mission control to pick classes, groups, parallelism.

·        Built-in grouping, dependencies, retries, data providers, and HTML reports.

·        Perfectly complements Selenium: parallel browsers + isolated drivers.

Feature

TestNG

JUnit 5 (quick view)

Data-driven

@DataProvider (native)

Parameterized tests (good, but different)

Dependencies

dependsOnMethods / groups

Not natively (workarounds)

Parallel

XML-driven, fine-grained

Extensions / config needed

Listeners

Rich (ITest, ISuite, IReporter...)

Extensions model

XML Orchestration

Strong & readable

Different philosophy (no XML by default)


 

4) Annotations & Lifecycle (a.k.a. The Test House) ๐Ÿ 

Memorize the flow: Suite → Test → Class → Method. (Guests enter rooms; we make the bed before each guest.)

public class TestLifecycleDemo {
  @BeforeSuite  public void beforeSuite(){ System.out.println("๐Ÿ”ง Suite setup"); }
  @BeforeTest   public void beforeTest(){ System.out.println("๐Ÿงฑ Test block setup"); }
  @BeforeClass  public void beforeClass(){ System.out.println("๐Ÿšช Class setup"); }
  @BeforeMethod public void beforeMethod(){ System.out.println("๐Ÿ›️ Make bed"); }

  @Test public void t1(){ System.out.println("๐Ÿ˜ด Guest 1 sleeps"); }
  @Test public void t2(){ System.out.println("๐Ÿ’ค Guest 2 naps"); }

  @AfterMethod public void afterMethod(){ System.out.println("๐Ÿงน Clean bed"); }
  @AfterClass  public void afterClass(){ System.out.println("๐Ÿšฟ Class cleanup"); }
  @AfterTest   public void afterTest(){ System.out.println("๐Ÿงผ Test block cleanup"); }
  @AfterSuite  public void afterSuite(){ System.out.println("๐Ÿ All done"); }
}

·        @BeforeGroups/@AfterGroups: run setup/teardown only for specific groups.

·        @Parameters & @Optional: inject values from XML.

·        alwaysRun=true: force config even if dependencies failed (carefully!).

·        timeOut, invocationCount, threadPoolSize: performance & flakiness levers.


 

5) Assertions: Hard vs Soft (Comedy of Errors) 

@Test
public void hardAssertions() {
  Assert.assertEquals(2+2, 4, "Math should behave today.");
  Assert.assertTrue("pizza".contains("piz"), "Where did pizza go?");
}

@Test
public void softAssertions() {
  SoftAssert s = new SoftAssert();
  s.assertEquals("actual", "expected", "First joke fell flat");
  s.assertTrue(false, "Second joke failed");
  s.assertNotNull(null, "Third joke invisible");
  s.assertAll(); // reveals all the chaos at once
}

Tip: Prefer precise, human-friendly messages; you’ll thank yourself at 3 AM CI failures.


 

6) @Parameters & @Optional (Config from XML) ๐Ÿงฉ

// testng.xml
<suite name="Suite">
  <parameter name="baseUrl" value="https://staging.example.com"/>
  <test name="UI">
    <parameter name="browser" value="chrome"/>
    <classes>
      <class name="com.example.tests.LoginTests"/>
    </classes>
  </test>
</suite>

public class ParamDemo {
  @Parameters({"baseUrl","browser"})
  @Test
  public void demo(@Optional("http://localhost") String baseUrl,
                   @Optional("chrome") String browser) {
    System.out.println("Base: " + baseUrl + " Browser: " + browser);
  }
}

Use -D overrides: mvn test -DbaseUrl=https://prod.example.com -Dbrowser=edge


 

7) DataProviders (Your Buffet Table) ๐Ÿฑ

Basic

@DataProvider(name="loginData")
public Object[][] loginData(){ return new Object[][] {
  {"valid","valid",true},
  {"invalid","valid",false},
  {"valid","invalid",false}
};}

@Test(dataProvider="loginData")
public void login(String u, String p, boolean ok){
  Assert.assertEquals(auth(u,p), ok);
}

From external source (CSV)

@DataProvider(name="csvData")
public Object[][] csvData() throws Exception {
  List<Object[]> rows = new ArrayList<>();
  try (BufferedReader br = Files.newBufferedReader(Paths.get("src/test/resources/users.csv"))) {
    br.lines().skip(1).forEach(line -> {
      String[] t = line.split(",");
      rows.add(new Object[]{t[0], t[1], Boolean.parseBoolean(t[2])});
    });
  }
  return rows.toArray(new Object[0][0]);
}

Parallel DataProvider

@DataProvider(name="searchTerms", parallel=true)
public Object[][] searches(){ return new Object[][] { {"laptop"}, {"camera"}, {"coffee beans"} }; }


 

8) Groups, Dependencies, Priorities (Choreography) ๐Ÿ•บ

public class EcommerceFlow {
  @Test(groups={"smoke","login"}, priority=1)
  public void validLogin(){ /* ... */ }

  @Test(groups={"regression","search"}, dependsOnGroups={"login"})
  public void search(){ /* ... */ }

  @Test(groups={"regression","checkout"}, dependsOnMethods={"search"})
  public void checkout(){ /* ... */ }
}

Prefer dependencies over priorities for business-flow correctness. Priorities are hints, not guarantees across classes.


 

9) Parallel Execution (Four Lanes, No Crashes) ๐Ÿ›ฃ️

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel Suite" parallel="methods" thread-count="4">
  <test name="Browsers">
    <parameter name="browser" value="chrome"/>
    <classes>
      <class name="com.example.tests.SearchTests"/>
      <class name="com.example.tests.CheckoutTests"/>
    </classes>
  </test>
</suite>

Thread-safe WebDriver with ThreadLocal

public final class DriverManager {
  private static final ThreadLocal<WebDriver> TL = new ThreadLocal<>();
  public static WebDriver get(){ return TL.get(); }
  public static void set(WebDriver d){ TL.set(d); }
  public static void unload(){ TL.remove(); }
}

public class BaseTest {
  @Parameters({"browser"})
  @BeforeMethod(alwaysRun = true)
  public void setUp(@Optional("chrome") String browser){
    WebDriver driver;
    if ("chrome".equalsIgnoreCase(browser)) {
      io.github.bonigarcia.wdm.WebDriverManager.chromedriver().setup();
      driver = new org.openqa.selenium.chrome.ChromeDriver();
    } else if ("edge".equalsIgnoreCase(browser)) {
      io.github.bonigarcia.wdm.WebDriverManager.edgedriver().setup();
      driver = new org.openqa.selenium.edge.EdgeDriver();
    } else {
      io.github.bonigarcia.wdm.WebDriverManager.firefoxdriver().setup();
      driver = new org.openqa.selenium.firefox.FirefoxDriver();
    }
    driver.manage().window().maximize();
    DriverManager.set(driver);
  }

  @AfterMethod(alwaysRun = true)
  public void tearDown(){
    WebDriver d = DriverManager.get();
    if (d != null) { d.quit(); DriverManager.unload(); }
  }
}

Golden rules: no mutable static state, isolate test data, prefer method-level parallelism first, then scale up.


 

10) Selenium Integration Patterns (POM + Waits) ๐Ÿงฉ

public abstract class BasePage {
  protected WebDriver driver;
  protected WebDriverWait wait;
  public BasePage(WebDriver d){
    this.driver = d;
    this.wait = new WebDriverWait(d, java.time.Duration.ofSeconds(10));
  }
  protected WebElement $(By locator){
    return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
  }
}

public class LoginPage extends BasePage {
  private By user = By.id("username");
  private By pass = By.id("password");
  private By go   = By.cssSelector("button[type=submit]");

  public LoginPage(WebDriver d){ super(d); }

  public HomePage login(String u, String p){
    $(user).sendKeys(u);
    $(pass).sendKeys(p);
    $(go).click();
    return new HomePage(driver);
  }
}

Avoid PageFactory for new code; prefer explicit waits and clear, immutable locators.


 

11) testng.xml: Orchestrate Everything ๐Ÿง‘‍✈️

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Full Suite" verbose="1" parallel="tests" thread-count="3">
  <parameter name="env" value="staging"/>
  <listeners>
    <listener class-name="com.example.listeners.CustomTestListener"/>
    <listener class-name="com.example.listeners.RetryTransformer"/>
  </listeners>

  <test name="Smoke">
    <groups><run><include name="smoke"/></run></groups>
    <classes>
      <class name="com.example.tests.login.LoginTests"/>
      <class name="com.example.tests.product.ProductSmokeTests"/>
    </classes>
  </test>

  <test name="Regression" preserve-order="true">
    <groups><run><include name="regression"/></run></groups>
    <classes>
      <class name="com.example.tests.product.ProductTests"/>
      <class name="com.example.tests.order.CheckoutTests"/>
    </classes>
  </test>
</suite>

Use preserve-order for deterministic runs, but don’t rely on it for business logic — encode flows with dependencies.


 

12) Listeners, Transformers, Interceptors (Superpowers) ๐Ÿฆธ

ITestListener — screenshots, metrics, emojis

public class CustomTestListener implements ITestListener {
  @Override public void onTestFailure(ITestResult r){
    System.out.println("❌ FAILED: " + r.getMethod().getMethodName());
    takeScreenshot(r.getMethod().getMethodName());
  }
  private void takeScreenshot(String name){
    WebDriver d = DriverManager.get();
    if (d instanceof TakesScreenshot ts){
      byte[] bytes = ts.getScreenshotAs(OutputType.BYTES);
      // save bytes to file ...
    }
  }
}

IRetryAnalyzer — auto‑rerun flakes

public class Retry implements IRetryAnalyzer {
  private int count=0, max=1;
  public boolean retry(ITestResult r){
    return count++ < max;
  }
}

IAnnotationTransformer — apply retry to all @Test

public class RetryTransformer implements IAnnotationTransformer {
  @Override
  public void transform(ITestAnnotation a, Class testClass, Constructor testConstructor, Method testMethod){
    if (a.getRetryAnalyzer() == null) a.setRetryAnalyzer(Retry.class);
  }
}

IMethodInterceptor — reorder/filter

public class OnlySmokeInterceptor implements IMethodInterceptor {
  @Override
  public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext ctx){
    return methods.stream()
      .filter(m -> Arrays.asList(m.getMethod().getGroups()).contains("smoke"))
      .toList();
  }
}


 

13) Factories: Dynamic Test Generation ๐Ÿญ

public class BrowserTest {
  private final String browser;
  public BrowserTest(String b){ this.browser=b; }

  @Test
  public void featureCheck(){ System.out.println("Testing on " + browser); }
}

public class DynamicFactory {
  @Factory
  public Object[] create(){
    return new Object[]{ new BrowserTest("chrome"), new BrowserTest("firefox"), new BrowserTest("edge") };
  }
}

Use @Factory when you need constructor parameters (e.g., locale, tenant) to fan‑out tests.


 

14) Timeouts, Repeats & Exceptions ⏱️

@Test(timeOut=2000) // ms
public void shouldBeQuick(){ /* ... */ }

@Test(invocationCount=3, threadPoolSize=3)
public void hammerEndpoint(){ /* repeated in parallel */ }

@Test(expectedExceptions = { IllegalArgumentException.class })
public void throwsNicely(){ throw new IllegalArgumentException("You asked for pineapple pizza."); }

Don’t overuse expectedExceptions; prefer asserting thrown exceptions in unit tests and validating errors in UI tests.


 

15) Reporting ๐Ÿ“Š

·        Default HTML lives in test-output/, including emailable-report.html.

·        IReporter: build a custom HTML/JSON for dashboards.

·        Attach screenshots/HTML dumps on failure via listeners.

public class JsonSummaryReporter implements IReporter {
  @Override
  public void generateReport(List xmlSuites, List suites, String outputDirectory) {
    // Iterate suites -> results -> tests -> methods -> statuses and serialize summary
  }
}


 

16) CI, CLI & Profiles (Jenkins/GitHub Actions) ๐Ÿงฐ

# Run a single suite
mvn -q -DtestngXml=testng-smoke.xml test

# Override browser/env
mvn test -Dbrowser=edge -Denv=prod

# Run only a group
mvn test -Dgroups=smoke

# Skip tests (build only)
mvn -DskipTests package

CI tips: cache Maven repo, run headless in containers, archive test-output/, publish HTML report artifacts.


 

17) Selenium Grid & Cross‑Browser ๐ŸŒ

@Parameters({"browser"})
@BeforeMethod
public void setUp(@Optional("chrome") String browser) throws MalformedURLException {
  MutableCapabilities caps;
  switch(browser.toLowerCase()){
    case "firefox" -> caps = new org.openqa.selenium.firefox.FirefoxOptions();
    case "edge"    -> caps = new org.openqa.selenium.edge.EdgeOptions();
    default        -> caps = new org.openqa.selenium.chrome.ChromeOptions();
  }
  WebDriver driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), caps);
  DriverManager.set(driver);
}

Keep Grid nodes stateless. Use unique downloads/temp dirs per session. Tag tests by capability via groups or @Factory.


 

18) Mini E‑commerce Suite (Funny Edition) ๐Ÿ›️

public class BaseTest {
  @BeforeMethod public void open(){
    DriverManager.get().get(System.getProperty("baseUrl","https://demo.shop"));
  }
  @AfterMethod public void clean(){
    // cookies/session cleanup if needed
  }
}

public class LoginTests extends BaseTest {
  @Test(groups={"smoke","login"})
  public void validLogin(){
    new LoginPage(DriverManager.get()).login("valid","valid");
  }
}

public class CartTests extends BaseTest {
  @Test(dependsOnGroups={"login"}, groups={"regression","cart"})
  public void addToCart(){
    // Add a 'coffee mug' because tests run better with coffee
  }
  @Test(dependsOnMethods={"addToCart"}, groups={"regression","checkout"})
  public void payWithCard(){
    // Enter card 4111...; assert receipt appears
  }
}

Yes, we bought a mug. No, finance won’t reimburse test mugs. ☕


 

19) Troubleshooting ๐Ÿงฏ

·        “My test didn’t run” → Check @Test present, groups filters in XML, method visibility = public.

·        Parallel NoSuchSessionException → Ensure ThreadLocal driver + independent page objects.

·        StaleElementReferenceException → Use explicit waits; re‑locate elements after DOM updates.

·        DataProvider mismatch → Provider parameters must align with @Test signature.

·        Dependencies cause skip → Review dependsOnMethods/groups; consider alwaysRun for cleanup.

If all else fails, take a snack break. Even CPUs cool down.


 

20) Best Practices Checklist ✅

·        One assertion per behavior (but allow multiple checks if logically cohesive).

·        Name groups meaningfully: smoke, regression, api, ui, checkout, login.

·        Keep tests independent; no hidden ordering assumptions.

·        Use Test Data Builders; avoid shared mutable state.

·        Prefer method‑level parallelism first; scale thoughtfully.

·        Capture rich failure context (screenshots, page source, console logs).

·        Quarantine flaky tests; add Retry only as a short‑term band‑aid.

·        Document testng.xml; keep suite files versioned and reviewed.

·        Run a tiny smoke on every PR; run full regression nightly.

You’re now dangerous (in a good way). Go ship reliable, speedy tests — and remember: the only flaky thing should be your croissant, not your build. ๐Ÿฅ


Comments