Selenium
Intermediate 16 min read Selenium

TestNG & JUnit Integration

A raw Selenium script is a script; a test framework makes it a suite. Learn to wrap WebDriver in TestNG and JUnit 5: lifecycle methods, assertions, data providers, parameterized tests, XML suites and parallel execution.

TestNGJUnit@BeforeMethodDataProvidertest suiteassertionsparallel
Introduction

What is this?

TestNG and JUnit give WebDriver automation a test lifecycle: setup runs before each test, cleanup after, and the framework reports pass/fail/skip with rich assertions. TestNG adds data providers for data-driven tests, dependency groups, and XML suites that control parallel execution; JUnit 5 offers parameterized tests and extensions. A shared base class initializes the driver in @BeforeMethod and quits it in @AfterMethod so individual tests stay focused on behavior.

Motivation

Why do you need it?

  • Lifecycle hooks (before/after) guarantee the driver starts fresh and always quits, even on failure.
  • Assertions with failure messages make test reports actionable instead of raw exceptions.
  • Data providers and parameterized tests run the same scenario across many inputs without code duplication.
  • XML suites (TestNG) define groups, listeners and parallelism without touching test code.
  • CI integrates trivially: mvn test or gradle test produce JUnit XML reports consumed by dashboards.
Think about it

Mental model

Imagine

A production line with check-in and check-out

Think of

Test lifecycle methods

Every part is inspected and registered before work starts and cleaned up after, whether it passed or was scrapped. @BeforeMethod and @AfterMethod do the same for every test.

Mechanics

How it works

  • @BeforeMethod launches the driver and navigates to the app under test; @AfterMethod quits it, so the browser never leaks between tests.
  • Assertions (Assert.assertEquals, assertEquals with a message) record failures while the framework keeps running the rest of the suite.
  • A TestNG @DataProvider feeds rows of arguments into a @Test(method), generating one test execution per row.
  • A TestNG XML suite lists classes, groups and a parallel="methods" or parallel="classes" attribute.
  • JUnit 5 uses @ParameterizedTest with @ValueSource/@CsvSource, and extensions like the Selenium JUnit extension for lifecycle management.

One test method's lifecycle under a test framework.

TestNG/JUnit runner
BeforeMethod
Test method
Assertion result
AfterMethod
Report
Reference

Key commands

mvn test

Run all tests through Maven Surefire.

Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
mvn test -DsuiteXmlFile=src/test/resources/testng.xml

Run a specific TestNG XML suite with its groups and parallel settings.

mvn test -Dtest=LoginTest -DfailIfNoTests=false

Run only the LoginTest class regardless of the Surefire default pattern.

gradlew test

Run the JUnit 5 suite through Gradle.

python -m pytest -k login --html=report.html

Run only tests matching 'login' and write an HTML report (pytest-html).

Hands-on

Examples

A data-driven login test with TestNG

A data-driven login test with TestNG
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class LoginTest {
WebDriver driver;
@BeforeMethod
public void setup() {
driver = new ChromeDriver();
driver.get("https://example.com/login");
}
@AfterMethod
public void teardown() {
if (driver != null) driver.quit();
}
@DataProvider(name = "users")
public Object[][] users() {
return new Object[][] {
{ "alice@example.com", "wrong", "Invalid credentials" },
{ "bob@example.com", "secret", "Welcome back" },
};
}
@Test(dataProvider = "users")
public void loginShowsExpectedMessage(String email, String password, String expected) {
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.cssSelector("button[type='submit']")).click();
Assert.assertEquals(
driver.findElement(By.id("message")).getText(), expected);
}
}

setup() and teardown() wrap every test method, so the driver is created fresh and always quit. The @DataProvider returns rows of credentials and expected messages; each row becomes its own test execution. The assertion compares the visible message with the expected value, and the framework records which row failed.

Real world

In production

  • Run data-driven login, checkout and search tests that cover many inputs from one method.
  • Use TestNG groups to run only smoke tests on every deploy and the full regression nightly.
  • Feed JUnit XML reports into CI dashboards to track flakiness over time.
  • Run parallel='methods' with a thread-safe driver factory to shrink suite duration.
Guidelines

Best practices

  • Put driver creation and cleanup in shared base classes, never in each test.
  • Use ThreadLocal<WebDriver> when tests run in parallel so each thread owns its browser.
  • Write assertions with descriptive messages naming the expected and actual state.
  • Configure fail-fast for smoke sets but let regression suites run to completion.
  • Keep one concern per test class: login tests, cart tests, checkout tests.
Watch out

Common mistakes

  • Creating the driver inside @Test itself, duplicating setup and leaking sessions on failure.
  • Sharing a single driver across parallel threads — two tests fight over one browser.
  • Swallowing exceptions in teardown and never discovering the driver leaked.
  • Hard-coding environment URLs in tests instead of reading config or system properties.
Root cause analysis

Common issues

Error
cannot find class in classpath: LoginTest
Root cause

The test source was not compiled or the package path is wrong.

Fix

Run mvn clean test and verify the class lives under src/test/java in the declared package.

Error
Only one WebDriver instance allowed
Root cause

Multiple parallel threads share a single static driver.

Fix

Use ThreadLocal<WebDriver> and a parallel-safe lifecycle in the base class.

Error
Suite XML contains duplicate tests
Root cause

The same class appears in the suite XML and the default Surefire pattern.

Fix

Exclude the class from default includes or remove the duplication from the XML.

When things break

Debugging tips

  • Read the JUnit XML / Surefire report for the exact assertion message and stack of each failure.
  • Add a screenshot-and-attach listener so failure reports carry visual context.
  • Isolate a flaky test with -Dtest=ClassName#method or pytest -k.
  • Enable TestNG verbose=2 to see the lifecycle events around the failing test.
Interviews

Interview checkpoint

Practice

Hands-on lab

Wrap Selenium in a TestNG Suite
35 min · intermediate
In progress
Objectives
  • Create a Maven project with Selenium and TestNG dependencies
  • Build a base class that owns driver setup and teardown
  • Write a data-driven test with @DataProvider
  • Run the suite through a TestNG XML file

Wrap Selenium in a TestNG Suite

35 min · intermediate

0/6 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/3
1

Which TestNG hook runs after each test and is the right place to quit the driver?

2

TestNG runs every method annotated @Test in its own thread by default.

3

Parallel tests randomly fail with 'Session ID is null'. What is the cause and fix?

Quick reference

Cheat sheet

TestNG
@Test(dataProvider = "name")Data-driven test method
@BeforeMethod / @AfterMethodPer-test setup and cleanup
@DataProviderFeed argument rows to a test
parallel="methods"Parallel execution in the XML suite
Assert.assertEquals(actual, expected)Failure-safe assertion
JUnit 5
@BeforeEach / @AfterEachPer-test setup and cleanup
@ParameterizedTestRun a test with multiple inputs
@ValueSource(strings = {...})Simple argument list
@CsvSourceTabular input rows
@TestInstance(PER_CLASS)Share state within a class
Go deeper

Official documentation