TL;DR

  • TestNG is a powerful Java testing framework with rich annotation support
  • Key annotations: @Test, @BeforeMethod, @AfterMethod, @DataProvider
  • Configure test suites via testng.xml for grouping, parallelization, parameters
  • Data providers enable data-driven testing with multiple input sets
  • Integrates seamlessly with Selenium WebDriver for browser automation

Best for: Java developers, Selenium automation, enterprise test frameworks Skip if: Using Kotlin (prefer Kotest) or simple unit tests (JUnit sufficient) Reading time: 15 minutes

Your Selenium tests run sequentially. 500 tests take 2 hours. CI/CD pipeline blocks every PR. Developers ignore test results because feedback comes too late.

TestNG solves test execution problems. Parallel execution, flexible configuration, data-driven testing — all built-in. No additional frameworks needed.

This tutorial covers TestNG from setup to advanced features — everything for building scalable test automation.

What is TestNG?

TestNG (Test Next Generation) is a testing framework for Java inspired by JUnit and NUnit. It goes beyond simple unit testing with features designed for integration and end-to-end testing.

Why TestNG:

  • Rich annotations — @Test, @Before/@After at multiple levels
  • Parallel execution — built-in support for concurrent testing
  • Data providers — elegant data-driven testing
  • Test dependencies — control execution order
  • XML configuration — flexible suite organization
  • HTML reports — built-in test reporting

Setup

Maven

<dependencies>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <suiteXmlFiles>
                    <suiteXmlFile>testng.xml</suiteXmlFile>
                </suiteXmlFiles>
            </configuration>
        </plugin>
    </plugins>
</build>

Gradle

dependencies {
    testImplementation 'org.testng:testng:7.9.0'
}

test {
    useTestNG()
}

Basic Annotations

@Test

import org.testng.annotations.Test;
import static org.testng.Assert.*;

public class CalculatorTest {

    @Test
    public void testAddition() {
        Calculator calc = new Calculator();
        assertEquals(calc.add(2, 3), 5);
    }

    @Test
    public void testSubtraction() {
        Calculator calc = new Calculator();
        assertEquals(calc.subtract(5, 3), 2);
    }

    @Test(enabled = false)  // Skip this test
    public void testDivision() {
        // Not implemented yet
    }

    @Test(expectedExceptions = ArithmeticException.class)
    public void testDivideByZero() {
        Calculator calc = new Calculator();
        calc.divide(10, 0);
    }
}

@Before and @After

import org.testng.annotations.*;

public class DatabaseTest {

    private Connection connection;

    @BeforeSuite
    public void setupSuite() {
        // Runs once before all tests in suite
        System.out.println("Starting test suite");
    }

    @BeforeClass
    public void setupClass() {
        // Runs once before first test in class
        connection = Database.connect();
    }

    @BeforeMethod
    public void setupMethod() {
        // Runs before each @Test method
        connection.beginTransaction();
    }

    @Test
    public void testInsert() {
        // Test code
    }

    @Test
    public void testUpdate() {
        // Test code
    }

    @AfterMethod
    public void teardownMethod() {
        // Runs after each @Test method
        connection.rollback();
    }

    @AfterClass
    public void teardownClass() {
        // Runs once after last test in class
        connection.close();
    }

    @AfterSuite
    public void teardownSuite() {
        // Runs once after all tests in suite
        System.out.println("Suite completed");
    }
}

Execution Order

@BeforeSuite
  @BeforeClass
    @BeforeMethod
      @Test (method 1)
    @AfterMethod
    @BeforeMethod
      @Test (method 2)
    @AfterMethod
  @AfterClass
@AfterSuite

Data Providers

Basic Data Provider

import org.testng.annotations.*;

public class LoginTest {

    @DataProvider(name = "loginCredentials")
    public Object[][] provideCredentials() {
        return new Object[][] {
            {"user1@example.com", "password1", true},
            {"user2@example.com", "password2", true},
            {"invalid@example.com", "wrongpass", false},
            {"", "password", false},
            {"user@example.com", "", false}
        };
    }

    @Test(dataProvider = "loginCredentials")
    public void testLogin(String email, String password, boolean expectedResult) {
        LoginPage loginPage = new LoginPage(driver);
        boolean result = loginPage.login(email, password);
        assertEquals(result, expectedResult,
            "Login failed for: " + email);
    }
}

External Data Provider

public class TestDataProvider {

    @DataProvider(name = "csvData")
    public static Object[][] readCsvData() throws IOException {
        List<Object[]> data = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(
                new FileReader("testdata/users.csv"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] values = line.split(",");
                data.add(values);
            }
        }
        return data.toArray(new Object[0][]);
    }
}

// Use in another class
public class UserTest {
    @Test(dataProvider = "csvData", dataProviderClass = TestDataProvider.class)
    public void testUserCreation(String name, String email, String role) {
        // Test with CSV data
    }
}

Test Configuration (testng.xml)

Basic Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">

<suite name="Regression Suite" verbose="1">

    <test name="Login Tests">
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.LogoutTest"/>
        </classes>
    </test>

    <test name="Dashboard Tests">
        <classes>
            <class name="com.example.tests.DashboardTest"/>
        </classes>
    </test>

</suite>

Package-Based Selection

<suite name="Full Suite">
    <test name="All Tests">
        <packages>
            <package name="com.example.tests.*"/>
        </packages>
    </test>
</suite>

Parameters

<suite name="Cross Browser Suite">
    <test name="Chrome Tests">
        <parameter name="browser" value="chrome"/>
        <parameter name="baseUrl" value="https://example.com"/>
        <classes>
            <class name="com.example.tests.LoginTest"/>
        </classes>
    </test>

    <test name="Firefox Tests">
        <parameter name="browser" value="firefox"/>
        <parameter name="baseUrl" value="https://example.com"/>
        <classes>
            <class name="com.example.tests.LoginTest"/>
        </classes>
    </test>
</suite>
public class LoginTest {
    private WebDriver driver;

    @Parameters({"browser", "baseUrl"})
    @BeforeMethod
    public void setup(String browser, String baseUrl) {
        if (browser.equals("chrome")) {
            driver = new ChromeDriver();
        } else if (browser.equals("firefox")) {
            driver = new FirefoxDriver();
        }
        driver.get(baseUrl);
    }

    @Test
    public void testLogin() {
        // Test runs with configured browser
    }
}

Parallel Execution

Configuration Options

<!-- Parallel by methods -->
<suite name="Parallel Suite" parallel="methods" thread-count="4">
    <test name="Tests">
        <classes>
            <class name="com.example.tests.LoginTest"/>
        </classes>
    </test>
</suite>

<!-- Parallel by classes -->
<suite name="Parallel Suite" parallel="classes" thread-count="4">
    <test name="Tests">
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.DashboardTest"/>
        </classes>
    </test>
</suite>

<!-- Parallel by tests -->
<suite name="Parallel Suite" parallel="tests" thread-count="2">
    <test name="Test Group 1">
        <classes>
            <class name="com.example.tests.LoginTest"/>
        </classes>
    </test>
    <test name="Test Group 2">
        <classes>
            <class name="com.example.tests.DashboardTest"/>
        </classes>
    </test>
</suite>

Thread-Safe WebDriver

public class BaseTest {
    protected static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    @BeforeMethod
    public void setup() {
        WebDriver webDriver = new ChromeDriver();
        driver.set(webDriver);
    }

    @AfterMethod
    public void teardown() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove();
        }
    }

    protected WebDriver getDriver() {
        return driver.get();
    }
}

public class LoginTest extends BaseTest {
    @Test
    public void testLogin() {
        getDriver().get("https://example.com");
        // Test is thread-safe
    }
}

Test Groups

Defining Groups

public class UserTest {

    @Test(groups = {"smoke", "regression"})
    public void testUserLogin() {
        // Critical test - runs in both groups
    }

    @Test(groups = {"regression"})
    public void testUserProfile() {
        // Only runs in regression
    }

    @Test(groups = {"regression", "slow"})
    public void testUserReports() {
        // Slow test
    }
}

Running Groups

<suite name="Smoke Suite">
    <test name="Smoke Tests">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <packages>
            <package name="com.example.tests.*"/>
        </packages>
    </test>
</suite>

<!-- Exclude slow tests -->
<suite name="Fast Suite">
    <test name="Fast Tests">
        <groups>
            <run>
                <include name="regression"/>
                <exclude name="slow"/>
            </run>
        </groups>
        <packages>
            <package name="com.example.tests.*"/>
        </packages>
    </test>
</suite>

Test Dependencies

public class OrderTest {

    @Test
    public void testLogin() {
        // Login first
    }

    @Test(dependsOnMethods = {"testLogin"})
    public void testAddToCart() {
        // Runs after login succeeds
    }

    @Test(dependsOnMethods = {"testAddToCart"})
    public void testCheckout() {
        // Runs after add to cart succeeds
    }

    @Test(dependsOnMethods = {"testCheckout"})
    public void testPayment() {
        // Final step
    }
}

Soft Dependencies

@Test(dependsOnMethods = {"testLogin"}, alwaysRun = true)
public void testLogout() {
    // Runs even if testLogin fails
}

Selenium Integration

Complete Example

public class SeleniumBaseTest {
    protected WebDriver driver;
    protected WebDriverWait wait;

    @BeforeMethod
    public void setup() {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        driver = new ChromeDriver(options);
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    }

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

public class LoginTest extends SeleniumBaseTest {

    @Test(groups = {"smoke"})
    public void testSuccessfulLogin() {
        driver.get("https://example.com/login");

        driver.findElement(By.id("email")).sendKeys("user@example.com");
        driver.findElement(By.id("password")).sendKeys("password123");
        driver.findElement(By.id("login-btn")).click();

        WebElement welcome = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.id("welcome"))
        );
        assertTrue(welcome.isDisplayed());
        assertTrue(welcome.getText().contains("Welcome"));
    }

    @DataProvider(name = "invalidLogins")
    public Object[][] invalidLoginData() {
        return new Object[][] {
            {"", "password", "Email is required"},
            {"user@example.com", "", "Password is required"},
            {"invalid", "password", "Invalid email format"}
        };
    }

    @Test(dataProvider = "invalidLogins", groups = {"regression"})
    public void testInvalidLogin(String email, String password, String expectedError) {
        driver.get("https://example.com/login");

        driver.findElement(By.id("email")).sendKeys(email);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.id("login-btn")).click();

        WebElement error = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.className("error"))
        );
        assertEquals(error.getText(), expectedError);
    }
}

TestNG with AI Assistance

AI tools can help write and optimize TestNG tests.

What AI does well:

  • Generate test methods from requirements
  • Create data providers with edge cases
  • Suggest assertion patterns
  • Convert JUnit tests to TestNG

What still needs humans:

  • Understanding business logic
  • Choosing test granularity
  • Balancing coverage vs maintenance
  • Debugging flaky tests

FAQ

What is TestNG?

TestNG (Test Next Generation) is a Java testing framework inspired by JUnit and NUnit. It provides powerful features beyond simple unit testing: annotations for lifecycle management, parallel test execution, data-driven testing via data providers, test dependencies, and flexible XML configuration for organizing test suites. It’s widely used with Selenium for browser automation.

TestNG vs JUnit — which is better?

TestNG offers more built-in features: parallel execution, test dependencies, data providers, and flexible XML configuration. JUnit 5 has added many similar features, narrowing the gap. TestNG remains popular for Selenium projects due to its XML suite configuration and established ecosystem. For new projects, either works well — choose based on team familiarity.

Is TestNG free?

Yes, TestNG is completely free and open-source under the Apache 2.0 license. There are no paid versions, enterprise editions, or premium features. All functionality — parallel execution, data providers, reporting — is available at no cost.

Can TestNG run tests in parallel?

Yes, TestNG has excellent built-in parallel execution support. Configure parallelism at method, class, or test level via testng.xml. Set thread count based on your machine capabilities. For Selenium tests, use ThreadLocal to ensure thread-safe WebDriver instances. Parallel execution can dramatically reduce test suite runtime.

Official Resources

See Also