Selenium with TestNG
Annotations, Assertions, DataProvider & XML
Introduction to TestNG
TestNG is a powerful testing framework inspired by JUnit and NUnit. It provides better test configuration, parallel execution, data-driven testing, and detailed reporting capabilities.
Setup & Installation
Maven Dependencies (pom.xml)
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
<scope>test</scope>
</dependency>
<!-- Selenium WebDriver -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.27.0</version>
</dependency>
Gradle Dependencies (build.gradle)
dependencies {
testImplementation 'org.testng:testng:7.10.2'
implementation 'org.seleniumhq.selenium:selenium-java:4.27.0'
}
TestNG Annotations
Annotations define test execution flow and setup/teardown operations.
Annotation Hierarchy
Annotation | Execution Order | Description
--------------------+-----------------+--------------------------------
@BeforeSuite | 1 | Runs once before all tests
@BeforeTest | 2 | Runs before each test tag
@BeforeClass | 3 | Runs once before test class
@BeforeMethod | 4 | Runs before each @Test method
@Test | 5 | The actual test method
@AfterMethod | 6 | Runs after each @Test method
@AfterClass | 7 | Runs once after test class
@AfterTest | 8 | Runs after each test tag
@AfterSuite | 9 | Runs once after all tests
Basic Test Class
import org.testng.annotations.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class LoginTest {
WebDriver driver;
@BeforeSuite
public void beforeSuite() {
System.out.println("Before Suite - Setup global config");
}
@BeforeClass
public void setupClass() {
System.out.println("Before Class - Initialize driver");
driver = new ChromeDriver();
}
@BeforeMethod
public void beforeMethod() {
System.out.println("Before Method - Navigate to URL");
driver.get("https://example.com");
}
@Test
public void testLogin() {
System.out.println("Executing test");
// Test logic here
}
@AfterMethod
public void afterMethod() {
System.out.println("After Method - Clear cookies");
driver.manage().deleteAllCookies();
}
@AfterClass
public void tearDownClass() {
System.out.println("After Class - Close driver");
driver.quit();
}
@AfterSuite
public void afterSuite() {
System.out.println("After Suite - Cleanup");
}
}
Annotation Tips
- Use
@BeforeMethodfor common setup before each test - Use
@BeforeClassfor one-time setup like driver initialization - Use
@AfterMethodfor cleanup after each test - Use
@AfterClassto close browser and release resources
Writing Test Methods
Simple Test
@Test
public void testPageTitle() {
driver.get("https://www.google.com");
String title = driver.getTitle();
Assert.assertEquals(title, "Google");
}
Test with Priority
// Tests execute in priority order (lower first)
@Test(priority = 1)
public void testLogin() {
// Login test
}
@Test(priority = 2)
public void testDashboard() {
// Dashboard test - runs after login
}
Test with Description
@Test(description = "Verify user can login with valid credentials")
public void testValidLogin() {
// Test implementation
}
Dependent Tests
@Test
public void testLogin() {
// Login functionality
}
@Test(dependsOnMethods = {"testLogin"})
public void testAddToCart() {
// Only runs if testLogin passes
}
Enable/Disable Tests
@Test(enabled = false)
public void testUnderDevelopment() {
// This test will be skipped
}
Timeout for Tests
@Test(timeOut = 5000) // 5 seconds
public void testPageLoad() {
driver.get("https://example.com");
// Fails if execution takes more than 5 seconds
}
Expected Exceptions
@Test(expectedExceptions = NoSuchElementException.class)
public void testElementNotFound() {
driver.findElement(By.id("nonExistent"));
// Test passes if NoSuchElementException is thrown
}
Assertions in TestNG
Assertions validate expected outcomes. TestNG provides both hard and soft assertions.
Hard Assertions
import org.testng.Assert;
@Test
public void testAssertions() {
// Equality
Assert.assertEquals(actual, expected);
Assert.assertEquals(driver.getTitle(), "Expected Title");
// Boolean
Assert.assertTrue(condition);
Assert.assertFalse(condition);
// Null checks
Assert.assertNull(object);
Assert.assertNotNull(object);
// With custom message
Assert.assertEquals(actual, expected, "Values don't match!");
}
Common Assertion Methods
// Compare values
Assert.assertEquals(actual, expected);
Assert.assertNotEquals(actual, expected);
// Check conditions
Assert.assertTrue(element.isDisplayed());
Assert.assertFalse(element.isEnabled());
// Check null/not null
Assert.assertNull(driver.findElement(By.id("id")));
Assert.assertNotNull(element);
// Fail test explicitly
Assert.fail("Test failed due to timeout");
Soft Assertions
import org.testng.asserts.SoftAssert;
@Test
public void testSoftAssertions() {
SoftAssert softAssert = new SoftAssert();
// These continue even if they fail
softAssert.assertEquals(driver.getTitle(), "Title");
softAssert.assertTrue(false, "This will fail but test continues");
softAssert.assertNotNull(null, "Another failure");
// Must call assertAll() to mark test as failed
softAssert.assertAll();
}
Data-Driven Testing
DataProvider allows running the same test with different data sets.
Basic DataProvider
@DataProvider(name = "loginData")
public Object[][] getLoginData() {
return new Object[][] {
{"user1@test.com", "password123"},
{"user2@test.com", "password456"},
{"user3@test.com", "password789"}
};
}
@Test(dataProvider = "loginData")
public void testLogin(String email, String password) {
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("loginBtn")).click();
// Add assertions
}
DataProvider from Excel
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
@DataProvider(name = "excelData")
public Object[][] getExcelData() throws Exception {
String filePath = "./testdata/TestData.xlsx";
FileInputStream file = new FileInputStream(filePath);
Workbook workbook = WorkbookFactory.create(file);
Sheet sheet = workbook.getSheetAt(0);
int rowCount = sheet.getLastRowNum();
int colCount = sheet.getRow(0).getLastCellNum();
Object[][] data = new Object[rowCount][colCount];
for (int i = 0; i < rowCount; i++) {
Row row = sheet.getRow(i + 1);
for (int j = 0; j < colCount; j++) {
data[i][j] = row.getCell(j).toString();
}
}
workbook.close();
file.close();
return data;
}
DataProvider with Method
// DataProvider in different class
public class TestDataProvider {
@DataProvider(name = "searchData")
public static Object[][] getSearchData() {
return new Object[][] {
{"Selenium"},
{"TestNG"},
{"Java"}
};
}
}
// Using DataProvider from different class
public class SearchTest {
@Test(dataProvider = "searchData", dataProviderClass = TestDataProvider.class)
public void testSearch(String keyword) {
driver.findElement(By.name("q")).sendKeys(keyword);
// Add search logic
}
}
Parallel DataProvider
@DataProvider(name = "parallelData", parallel = true)
public Object[][] getParallelData() {
return new Object[][] {
{"Test1"},
{"Test2"},
{"Test3"}
};
}
DataProvider Best Practices
- Keep test data separate from test logic
- Use meaningful DataProvider names
- Store large datasets in external files (Excel, CSV, JSON)
- Use parallel execution for independent data sets
TestNG XML Configuration
TestNG XML files define test suites, organize tests, and configure execution parameters.
Basic testng.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Test Suite">
<test name="Login Tests">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
Multiple Test Classes
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression Suite">
<test name="All Tests">
<classes>
<class name="tests.LoginTest"/>
<class name="tests.HomePageTest"/>
<class name="tests.CheckoutTest"/>
</classes>
</test>
</suite>
Run Specific Methods
<suite name="Smoke Suite">
<test name="Smoke Tests">
<classes>
<class name="tests.LoginTest">
<methods>
<include name="testValidLogin"/>
<include name="testLogout"/>
</methods>
</class>
</classes>
</test>
</suite>
Package-Level Execution
<suite name="Package Suite">
<test name="Package Tests">
<packages>
<package name="tests.regression"/>
<package name="tests.smoke"/>
</packages>
</test>
</suite>
Parallel Execution
<suite name="Parallel Suite" parallel="tests" thread-count="3">
<test name="Chrome Tests">
<parameter name="browser" value="chrome"/>
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
<test name="Firefox Tests">
<parameter name="browser" value="firefox"/>
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
<!-- Parallel options:
tests - Run test tags in parallel
classes - Run test classes in parallel
methods - Run test methods in parallel
-->
Parameters in XML
<suite name="Parameterized Suite">
<parameter name="url" value="https://example.com"/>
<parameter name="browser" value="chrome"/>
<test name="Tests">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
Using Parameters in Test
import org.testng.annotations.Parameters;
public class LoginTest {
WebDriver driver;
@Parameters({"browser", "url"})
@BeforeClass
public void setup(String browser, String url) {
if (browser.equalsIgnoreCase("chrome")) {
driver = new ChromeDriver();
} else if (browser.equalsIgnoreCase("firefox")) {
driver = new FirefoxDriver();
}
driver.get(url);
}
}
Groups in XML
<suite name="Group Suite">
<test name="Smoke Tests">
<groups>
<run>
<include name="smoke"/>
</run>
</groups>
<classes>
<class name="tests.LoginTest"/>
<class name="tests.HomePageTest"/>
</classes>
</test>
</suite>
Test Groups
Groups allow you to categorize tests and run specific categories as needed.
Defining Groups
public class LoginTest {
@Test(groups = {"smoke", "regression"})
public void testValidLogin() {
// Test logic
}
@Test(groups = {"regression"})
public void testInvalidLogin() {
// Test logic
}
@Test(groups = {"smoke"})
public void testLogout() {
// Test logic
}
}
Group Dependencies
@Test(groups = {"database"})
public void testDatabaseConnection() {
// Setup database
}
@Test(groups = {"api"}, dependsOnGroups = {"database"})
public void testAPI() {
// API tests that need database
}
Class-Level Groups
@Test(groups = {"regression"})
public class CheckoutTest {
@Test
public void testAddToCart() {
// All methods inherit "regression" group
}
@Test
public void testCheckout() {
// This is also in "regression" group
}
}
Running Groups Programmatically
import org.testng.TestNG;
public class TestRunner {
public static void main(String[] args) {
TestNG testng = new TestNG();
testng.setTestClasses(new Class[]{LoginTest.class});
testng.setGroups("smoke");
testng.run();
}
}
Common Group Categories
- smoke: Quick tests to verify basic functionality
- regression: Comprehensive tests for all features
- sanity: Tests after minor changes
- critical: High-priority test cases
- ui: User interface tests
- api: API integration tests
TestNG Listeners
Listeners monitor test execution and perform actions at different stages like test start, success, failure, or skip.
ITestListener Implementation
import org.testng.ITestListener;
import org.testng.ITestResult;
public class TestListener implements ITestListener {
@Override
public void onTestStart(ITestResult result) {
System.out.println("Test Started: " + result.getName());
}
@Override
public void onTestSuccess(ITestResult result) {
System.out.println("Test Passed: " + result.getName());
}
@Override
public void onTestFailure(ITestResult result) {
System.out.println("Test Failed: " + result.getName());
// Capture screenshot on failure
captureScreenshot(result.getName());
}
@Override
public void onTestSkipped(ITestResult result) {
System.out.println("Test Skipped: " + result.getName());
}
private void captureScreenshot(String testName) {
// Screenshot logic here
}
}
Using Listener with Annotation
import org.testng.annotations.Listeners;
@Listeners(TestListener.class)
public class LoginTest {
@Test
public void testLogin() {
// Test logic
}
}
Using Listener in XML
<suite name="Suite">
<listeners>
<listener class-name="listeners.TestListener"/>
</listeners>
<test name="Tests">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
IRetryAnalyzer for Failed Tests
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
public class RetryAnalyzer implements IRetryAnalyzer {
private int retryCount = 0;
private static final int maxRetryCount = 2;
@Override
public boolean retry(ITestResult result) {
if (retryCount < maxRetryCount) {
retryCount++;
System.out.println("Retrying " + result.getName() +
" - Attempt " + retryCount);
return true;
}
return false;
}
}
// Using RetryAnalyzer
@Test(retryAnalyzer = RetryAnalyzer.class)
public void testFlaky() {
// Test that might fail intermittently
}
Custom Report Listener
import org.testng.IReporter;
import org.testng.ISuite;
import org.testng.xml.XmlSuite;
public class CustomReporter implements IReporter {
@Override
public void generateReport(List<XmlSuite> xmlSuites,
List<ISuite> suites,
String outputDirectory) {
// Custom report generation logic
System.out.println("Generating custom report...");
}
}
Parallel Test Execution
Run tests in parallel to reduce overall execution time.
Parallel at Test Level
<suite name="Parallel Suite" parallel="tests" thread-count="3">
<test name="Test1">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
<test name="Test2">
<classes>
<class name="tests.CheckoutTest"/>
</classes>
</test>
</suite>
Parallel at Class Level
<suite name="Parallel Suite" parallel="classes" thread-count="2">
<test name="All Tests">
<classes>
<class name="tests.LoginTest"/>
<class name="tests.HomePageTest"/>
<class name="tests.CheckoutTest"/>
</classes>
</test>
</suite>
Parallel at Method Level
<suite name="Parallel Suite" parallel="methods" thread-count="5">
<test name="All Tests">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
Thread-Safe Driver Setup
public class BaseTest {
protected static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
public static WebDriver getDriver() {
return driver.get();
}
@BeforeMethod
public void setUp() {
WebDriver webDriver = new ChromeDriver();
driver.set(webDriver);
getDriver().manage().window().maximize();
}
@AfterMethod
public void tearDown() {
if (getDriver() != null) {
getDriver().quit();
driver.remove();
}
}
}
Parallel Modes
Mode | Description | Thread Count
------------+--------------------------------+------------------
tests | Runs test tags in parallel | 2-5 recommended
classes | Runs test classes in parallel | 3-10 recommended
methods | Runs test methods in parallel | 5-20 recommended
instances | Runs instances in parallel | Varies
none | No parallel execution | 1
Test Reporting
TestNG generates detailed HTML reports automatically. You can also integrate third-party reporting tools.
Default Reports Location
test-output/
├── index.html // Main report
├── emailable-report.html // Email-friendly report
├── testng-results.xml // XML results
└── Suite/
├── Test.html
└── Test.xml
Extent Reports Integration
<!-- Add Extent Reports dependency -->
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.1.2</version>
</dependency>
Extent Reports Manager
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
public class ExtentManager {
private static ExtentReports extent;
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
public static ExtentReports createInstance() {
ExtentSparkReporter sparkReporter =
new ExtentSparkReporter("./reports/ExtentReport.html");
extent = new ExtentReports();
extent.attachReporter(sparkReporter);
return extent;
}
public static ExtentTest getTest() {
return test.get();
}
public static void setTest(ExtentTest extentTest) {
test.set(extentTest);
}
}
Using Extent Reports in Tests
public class LoginTest {
ExtentReports extent = ExtentManager.createInstance();
@BeforeMethod
public void setup() {
ExtentTest extentTest = extent.createTest("Login Test");
ExtentManager.setTest(extentTest);
}
@Test
public void testLogin() {
ExtentManager.getTest().info("Starting login test");
// Test logic
ExtentManager.getTest().pass("Login successful");
}
@AfterMethod
public void tearDown() {
extent.flush();
}
}
Report Features
- TestNG default reports show pass/fail/skip statistics
- Extent Reports provide rich HTML reports with screenshots
- Allure Reports offer interactive test execution dashboards
- Reports can be integrated into CI/CD pipelines
Best Practices
Complete Test Class Example
import org.testng.annotations.*;
import org.testng.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.By;
public class LoginTest {
WebDriver driver;
@BeforeClass
public void setup() {
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@BeforeMethod
public void navigateToLogin() {
driver.get("https://example.com/login");
}
@Test(priority = 1, description = "Verify login with valid credentials")
public void testValidLogin() {
driver.findElement(By.id("username")).sendKeys("testuser");
driver.findElement(By.id("password")).sendKeys("password123");
driver.findElement(By.id("loginBtn")).click();
Assert.assertTrue(driver.getCurrentUrl().contains("dashboard"),
"Login failed - Dashboard not displayed");
}
@Test(priority = 2, description = "Verify error message for invalid credentials")
public void testInvalidLogin() {
driver.findElement(By.id("username")).sendKeys("wronguser");
driver.findElement(By.id("password")).sendKeys("wrongpass");
driver.findElement(By.id("loginBtn")).click();
String errorMsg = driver.findElement(By.className("error")).getText();
Assert.assertEquals(errorMsg, "Invalid credentials");
}
@AfterMethod
public void clearSession() {
driver.manage().deleteAllCookies();
}
@AfterClass
public void teardown() {
if (driver != null) {
driver.quit();
}
}
}
Key Takeaways
- TestNG provides powerful test configuration with annotations
- DataProvider enables data-driven testing with multiple data sets
- TestNG XML allows flexible test suite configuration
- Listeners enable custom actions like screenshots on failure
- Parallel execution significantly reduces test execution time
- Groups help organize and selectively run tests
Next Steps
Continue Your Learning Journey
- Page Object Model (POM) - Design pattern for maintainable test automation
- Data-Driven Testing - Parameterize tests with Excel, CSV or database
- Selenium Grid - Parallel execution across multiple browsers and machines
- Extent Reports - Generate rich HTML reports with screenshots
- CI/CD Integration - Automate test execution with Jenkins or GitHub Actions
- BDD with Cucumber - Behavior-driven development with TestNG