Series Navigation
← Part 6: Waits — Implicit, Explicit and Fluent
→ Part 8: Page Object Model — Scalable Test Architecture
TestNG vs JUnit
Both are Java test frameworks. TestNG is preferred for Selenium because:
| Feature | TestNG | JUnit 4 |
|---|---|---|
| Parallel execution | ✅ Built-in, configurable via XML | ❌ Needs external plugins |
| Test grouping | ✅ groups attribute |
❌ Not supported |
| Data providers | ✅ @DataProvider |
Limited with @Parameterized |
| Dependency between tests | ✅ dependsOnMethods |
❌ Not supported |
| Suite-level configuration | ✅ testng.xml | ❌ No equivalent |
| Listeners | ✅ Powerful ITestListener |
Basic RunListener |
| Reports | ✅ Built-in HTML reports | External plugin needed |
All TestNG Annotations
import org.testng.annotations.*;
public class AnnotationDemo {
// ── SUITE LEVEL ──────────────────────────────────────────────────────────
@BeforeSuite // runs ONCE before any tests in the entire suite
public void beforeSuite() {
System.out.println("Suite starting — set up database, test data, etc.");
}
@AfterSuite // runs ONCE after ALL tests in the suite finish
public void afterSuite() {
System.out.println("Suite finished — clean up shared resources");
}
// ── TEST LEVEL (within a <test> tag in testng.xml) ───────────────────────
@BeforeTest // runs once before all tests in a <test> block
public void beforeTest() {
System.out.println("Before test block");
}
@AfterTest // runs once after all tests in a <test> block
public void afterTest() {
System.out.println("After test block");
}
// ── CLASS LEVEL ──────────────────────────────────────────────────────────
@BeforeClass // runs once before the first test method in this class
public void beforeClass() {
System.out.println("Class starting");
}
@AfterClass // runs once after the last test method in this class
public void afterClass() {
System.out.println("Class finished");
}
// ── METHOD LEVEL ─────────────────────────────────────────────────────────
@BeforeMethod // runs before EACH test method
public void beforeMethod() {
System.out.println("Setting up browser");
}
@AfterMethod // runs after EACH test method
public void afterMethod() {
System.out.println("Closing browser");
}
// ── TEST METHOD ──────────────────────────────────────────────────────────
@Test
public void myTest() {
System.out.println("Running test");
}
}
Execution order:
BeforeSuite
BeforeTest
BeforeClass
BeforeMethod → @Test → AfterMethod (repeated for each @Test)
BeforeMethod → @Test → AfterMethod
AfterClass
AfterTest
AfterSuite
@Test Attributes
// Basic test
@Test
public void basicTest() { }
// Custom test name for reports
@Test(description = "Verify user can login with valid credentials")
public void loginTest() { }
// Expected exception — test passes only if this exception is thrown
@Test(expectedExceptions = IllegalArgumentException.class)
public void invalidInputTest() {
throw new IllegalArgumentException("invalid");
}
// Expected exception with message
@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = ".*invalid.*")
public void invalidInputWithMessageTest() { }
// Timeout — test fails if not done in 5 seconds
@Test(timeOut = 5000)
public void performanceTest() { }
// Groups — categorise tests
@Test(groups = {"smoke", "login"})
public void smokeLoginTest() { }
// Priority — lower number runs first (default is 0)
@Test(priority = 1)
public void firstTest() { }
@Test(priority = 2)
public void secondTest() { }
// Dependencies — run after another test
@Test(dependsOnMethods = {"loginTest"})
public void dashboardTest() { }
// Dependencies on groups
@Test(dependsOnGroups = {"login"})
public void postLoginTest() { }
// Skip this test
@Test(enabled = false)
public void skippedTest() { }
// Run this test N times
@Test(invocationCount = 3)
public void retryTest() { }
// Run with N parallel threads
@Test(invocationCount = 5, threadPoolSize = 3)
public void parallelInvocationTest() { }
Test Groups and Filtering
// Assign tests to groups
@Test(groups = {"smoke"})
public void verifyHomePageLoads() { }
@Test(groups = {"smoke", "regression"})
public void verifyLoginWorks() { }
@Test(groups = {"regression"})
public void verifyAdvancedSearch() { }
@Test(groups = {"slow", "regression"})
public void verifyReportGeneration() { }
Run specific groups in testng.xml:
<!-- Run only smoke tests -->
<suite name="Smoke Suite">
<test name="Smoke Tests">
<groups>
<run>
<include name="smoke"/>
</run>
</groups>
<classes>
<class name="com.yourname.selenium.tests.LoginTest"/>
<class name="com.yourname.selenium.tests.HomeTest"/>
</classes>
</test>
</suite>
<!-- Run regression but exclude slow tests -->
<suite name="Regression Suite">
<test name="Fast Regression">
<groups>
<run>
<include name="regression"/>
<exclude name="slow"/>
</run>
</groups>
<packages>
<package name="com.yourname.selenium.tests"/>
</packages>
</test>
</suite>
Parallel Execution in testng.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<!-- parallel="methods": each test METHOD runs in its own thread -->
<suite name="Parallel Suite" parallel="methods" thread-count="4">
<test name="All Tests">
<packages>
<package name="com.yourname.selenium.tests"/>
</packages>
</test>
</suite>
<!-- parallel="classes": each test CLASS runs in its own thread -->
<suite name="Class Parallel Suite" parallel="classes" thread-count="3">
<test name="All Tests">
<packages>
<package name="com.yourname.selenium.tests"/>
</packages>
</test>
</suite>
<!-- parallel="tests": each <test> block runs in its own thread -->
<suite name="Browser Parallel Suite" parallel="tests" thread-count="3">
<test name="Chrome Tests">
<parameter name="browser" value="chrome"/>
<classes>
<class name="com.yourname.selenium.tests.LoginTest"/>
</classes>
</test>
<test name="Firefox Tests">
<parameter name="browser" value="firefox"/>
<classes>
<class name="com.yourname.selenium.tests.LoginTest"/>
</classes>
</test>
<test name="Edge Tests">
<parameter name="browser" value="edge"/>
<classes>
<class name="com.yourname.selenium.tests.LoginTest"/>
</classes>
</test>
</suite>
Read the browser parameter in BaseTest:
@BeforeMethod
@Parameters("browser")
public void setUp(@Optional("chrome") String browser) {
WebDriver driver = DriverFactory.createDriver(browser);
driverThreadLocal.set(driver);
}
@DataProvider — Parameterised Tests
import org.testng.annotations.DataProvider;
public class LoginTest extends BaseTest {
@DataProvider(name = "loginCredentials")
public Object[][] provideLoginData() {
return new Object[][] {
// { username, password, expectedResult }
{ "tomsmith", "SuperSecretPassword!", "success" },
{ "wronguser", "wrongpass", "failure" },
{ "tomsmith", "wrongpass", "failure" },
{ "", "", "failure" },
};
}
@Test(dataProvider = "loginCredentials",
description = "Login with various credential combinations")
public void testLoginCombinations(String username, String password, String expected) {
WebDriver driver = getDriver();
driver.get("https://the-internet.herokuapp.com/login");
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.cssSelector("button[type='submit']")).click();
WebElement flash = driver.findElement(By.id("flash"));
if (expected.equals("success")) {
Assert.assertTrue(flash.getText().contains("logged into a secure area"));
} else {
Assert.assertTrue(
flash.getText().contains("invalid") ||
flash.getText().contains("Your username is invalid"),
"Should show error for: " + username + "/" + password
);
}
}
}
ITestListener — Screenshots on Failure
// src/test/java/com/yourname/selenium/listeners/TestListener.java
package com.yourname.selenium.listeners;
import com.yourname.selenium.base.BaseTest;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.*;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TestListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
// Get driver from the test instance
Object testInstance = result.getInstance();
if (testInstance instanceof BaseTest) {
WebDriver driver = ((BaseTest) testInstance).getDriver();
if (driver != null) {
takeScreenshot(driver, result.getName());
}
}
}
private void takeScreenshot(WebDriver driver, String testName) {
try {
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String filename = "FAIL_" + testName + "_" + timestamp + ".png";
File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
File dest = new File("test-output/screenshots/" + filename);
FileUtils.copyFile(src, dest);
System.out.println("[SCREENSHOT] Saved: " + dest.getAbsolutePath());
} catch (IOException e) {
System.err.println("[SCREENSHOT] Failed to save: " + e.getMessage());
}
}
@Override
public void onTestStart(ITestResult result) {
System.out.println("▶ Starting: " + result.getName());
}
@Override
public void onTestSuccess(ITestResult result) {
System.out.println("✓ Passed: " + result.getName());
}
@Override
public void onTestSkipped(ITestResult result) {
System.out.println("⊘ Skipped: " + result.getName());
}
@Override
public void onStart(ITestContext context) {
System.out.println("=== Suite Starting: " + context.getName() + " ===");
new File("test-output/screenshots").mkdirs();
}
@Override
public void onFinish(ITestContext context) {
System.out.println("=== Suite Finished ===");
System.out.println("Passed: " + context.getPassedTests().size());
System.out.println("Failed: " + context.getFailedTests().size());
System.out.println("Skipped: " + context.getSkippedTests().size());
}
}
Register the listener in testng.xml:
<suite name="Full Suite">
<listeners>
<listener class-name="com.yourname.selenium.listeners.TestListener"/>
</listeners>
<test name="All Tests">
<packages>
<package name="com.yourname.selenium.tests"/>
</packages>
</test>
</suite>
What's Next
In Part 8 we build the Page Object Model — the design pattern that keeps Selenium code maintainable as your application and test suite grow. This is the most important architectural skill in Selenium.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.