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.
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.
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.
Mental model
A production line with check-in and check-out
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.
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.
Key commands
mvn testRun all tests through Maven Surefire.
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
mvn test -DsuiteXmlFile=src/test/resources/testng.xmlRun a specific TestNG XML suite with its groups and parallel settings.
mvn test -Dtest=LoginTest -DfailIfNoTests=falseRun only the LoginTest class regardless of the Surefire default pattern.
gradlew testRun the JUnit 5 suite through Gradle.
python -m pytest -k login --html=report.htmlRun only tests matching 'login' and write an HTML report (pytest-html).
Examples
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.
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.
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.
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.
Common issues
cannot find class in classpath: LoginTestThe test source was not compiled or the package path is wrong.
Run mvn clean test and verify the class lives under src/test/java in the declared package.
Only one WebDriver instance allowedMultiple parallel threads share a single static driver.
Use ThreadLocal<WebDriver> and a parallel-safe lifecycle in the base class.
Suite XML contains duplicate testsThe same class appears in the suite XML and the default Surefire pattern.
Exclude the class from default includes or remove the duplication from the XML.
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.
Interview checkpoint
Hands-on lab
- 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
Quick quiz
Which TestNG hook runs after each test and is the right place to quit the driver?
TestNG runs every method annotated @Test in its own thread by default.
Parallel tests randomly fail with 'Session ID is null'. What is the cause and fix?
Cheat sheet
@Test(dataProvider = "name")Data-driven test method@BeforeMethod / @AfterMethodPer-test setup and cleanup@DataProviderFeed argument rows to a testparallel="methods"Parallel execution in the XML suiteAssert.assertEquals(actual, expected)Failure-safe assertion@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