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
Post a Comment