View all services
Talk to QA Advisor
/Blog/TestNG Tutorial (2027): Write and Run Your First Selenium Test, Step by Step
Automation Testing9 min read

TestNG Tutorial (2027): Write and Run Your First Selenium Test, Step by Step

Write and run your first Selenium test with TestNG: a Maven project, three tests on a practice login page, a real failure explained, the reports, Eclipse steps and the mistakes to avoid.

Published January 27, 2023Last updated October 28, 2026
On this page

TestNG is a Java testing framework, and it is the runner behind a large share of Selenium suites. If you are starting out in automation testing, it is a good first framework: it gives you setup and teardown methods, assertions, groups and reports without any extra libraries.

This tutorial takes you from an empty folder to a working Selenium test run by TestNG. You will create a Maven project, write three tests against a public practice login page, run them from the command line, read a real failure and find the reports. Every file and every output below comes from our own run on 28 October 2026, on Selenium 4.51.0, TestNG 7.12.0, JDK 21 and Maven 3.9.16, in Chrome 155 and Firefox 159.

When your first test works, our TestNG framework guide covers groups, data providers, dependencies and parallel runs.

Figure 1 · Before you start
Six things, four of them free downloads
Maven fetches Selenium and TestNG, so you install only the JDK, Maven and a browser.
JDK
17 or newer
We used Temurin 21.0.12. The project compiles for release 17.
Maven
3.9.x
We used 3.9.16. It downloads Selenium and TestNG for you.
Selenium
4.51.0
Released 9 October 2026. Includes Selenium Manager.
TestNG
7.12.0
The current release when we ran this.
Browser
Chrome or Firefox
We ran Chrome 155 and Firefox 159.
IDE
Optional
IntelliJ IDEA runs TestNG from the editor; Eclipse needs the TestNG plugin.
Versions from our run, 28 October 2026. Selenium release date: PyPI and Maven Central.

What has changed since this tutorial was first written

The first version of this tutorial, from 2016, used Selenium 2.53.0 with Firefox 45. You downloaded a jar by hand, added it to the Eclipse build path and kept it matched to one browser version. That setup does not work with any current browser.

Two changes make the modern version shorter. Maven downloads Selenium and TestNG from two lines in a file. And since Selenium 4.6, a tool called Selenium Manager ships with Selenium. Its documentation says it "automatically discovers, downloads, and caches the drivers required by Selenium". You no longer download ChromeDriver or GeckoDriver, and you no longer set a driver path.

Figure 2 · What changed
Most of the old setup is now automatic
The 2016 version of this tutorial spent two of its seven steps adding jars by hand.
Libraries
Then
Download jars by hand and add them to the build path
Now
Two dependencies in pom.xml; Maven downloads them
Browser driver
Then
Download the driver and set a system property to its path
Now
None: Selenium Manager finds or downloads it
Version matching
Then
Match the jar to one browser version (2.53 with Firefox 45)
Now
Selenium Manager matches the driver to your browser
Running
Then
IDE only: right-click, Run As
Now
mvn test from any terminal or CI job, or the IDE
Then: this post's earlier version (Selenium 2.53.0, Firefox 45). Now: Selenium Manager documentation, selenium.dev, read 28 October 2026.

Step 1: Create the Maven project

Make a folder called first-testng-test. Inside it, create the folders src/test/java/io/qable/firsttest. Maven expects test code under src/test/java, and the rest of the path is your package name.

In the project folder, create a file called pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.qable</groupId>
  <artifactId>first-testng-test</artifactId>
  <version>1.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <surefire.version>3.5.6</surefire.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>4.51.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.12.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${surefire.version}</version>
      </plugin>
    </plugins>
  </build>
</project>

Three parts matter. The two dependencies bring in Selenium and TestNG, both with test scope because only your tests use them. The Surefire plugin is what runs tests when you type mvn test, and it detects TestNG automatically. The release property compiles for Java 17, so any JDK from 17 upwards will build it.

We pinned Surefire to 3.5.6 for a reason covered later. For this project, the newest release, 3.6.1, ran the same three tests with the same results.

Step 2: Write your first TestNG class

Create LoginTest.java in src/test/java/io/qable/firsttest. It tests the login page of the-internet, a practice site published by Sauce Labs for exactly this purpose. Its username and password are printed on the page, so you can run this test yourself.

package io.qable.firsttest;

import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class LoginTest {

    private WebDriver driver;

    @BeforeMethod
    public void openBrowser() {
        // Selenium Manager finds or downloads the right driver: no driver path, no jar matching.
        String browser = System.getProperty("browser", "chrome");
        if (browser.equals("firefox")) {
            FirefoxOptions options = new FirefoxOptions();
            options.addArguments("-headless");
            driver = new FirefoxDriver(options);
        } else {
            ChromeOptions options = new ChromeOptions();
            options.addArguments("--headless=new");
            driver = new ChromeDriver(options);
        }
        Capabilities caps = ((RemoteWebDriver) driver).getCapabilities();
        System.out.println("Browser: " + caps.getBrowserName() + " " + caps.getBrowserVersion());
        driver.get("https://the-internet.herokuapp.com/login");
    }

    @Test
    public void validLoginOpensTheSecureArea() {
        driver.findElement(By.id("username")).sendKeys("tomsmith");
        driver.findElement(By.id("password")).sendKeys("SuperSecretPassword!");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        Assert.assertTrue(driver.getCurrentUrl().endsWith("/secure"), "should land on /secure");
        Assert.assertTrue(driver.findElement(By.id("flash")).getText().contains("You logged into a secure area!"));
    }

    @Test
    public void wrongPasswordShowsAnError() {
        driver.findElement(By.id("username")).sendKeys("tomsmith");
        driver.findElement(By.id("password")).sendKeys("wrong-password");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        Assert.assertTrue(driver.getCurrentUrl().endsWith("/login"), "should stay on /login");
        Assert.assertTrue(driver.findElement(By.id("flash")).getText().contains("Your password is invalid!"));
    }

    @Test
    public void flashMessageMatchesExactly() {
        // Written to show a real failure: the visible message carries more than the words.
        driver.findElement(By.id("username")).sendKeys("tomsmith");
        driver.findElement(By.id("password")).sendKeys("SuperSecretPassword!");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        Assert.assertEquals(driver.findElement(By.id("flash")).getText(), "You logged into a secure area!");
    }

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

Here is what each part does:

  • @BeforeMethod: runs before every test. It starts a new headless browser, prints the browser name and version, and opens the login page.
  • @Test: marks a test. TestNG finds these methods itself, so you never call them.
  • @AfterMethod: runs after every test, even a failed one. It closes the browser.
  • Assert: TestNG's assertion class. If an assertion fails, the test fails and the report shows the expected and the actual value.

The browser is chosen with a system property, so the same code runs in Chrome or Firefox. Headless mode runs the browser without a window, which is what a CI server needs. Remove the headless line if you want to watch the test run.

Two details in that setup catch beginners out. First, the headless switch is spelled differently for each browser: Chrome takes --headless=new, while Firefox takes -headless, with a single dash, as in our code. Second, System.getProperty("browser", "chrome") reads a value you pass on the Maven command line, and falls back to Chrome when you pass nothing. Surefire hands that value to the separate Java process that runs your tests, which is why our Firefox runs printed Browser: firefox 159.0 without any change to the code. The Browser: line itself is worth keeping in your own tests. When a result differs between two machines, the first question is which browser and version ran, and the log already answers it.

Two tests should pass. The third is written to fail, so you can see what a real failure looks like before you meet one in your own work.

Choosing the right setup annotation

@BeforeMethod is one of four setup annotations. They differ only in how often they run, and the TestNG documentation defines each one. Each has a matching @After annotation that runs at the same level when the work is done.

AnnotationWhen it runsUse it for
@BeforeSuiteOnce, before all tests in the suiteWork the whole run needs once, such as reading a configuration file
@BeforeTestOnce before each test tag in testng.xml, for all its classesSettings shared by several classes, such as the target environment
@BeforeClassOnce, before the first test method in the classExpensive setup that the class's tests can safely share
@BeforeMethodBefore every test methodA fresh browser for each test, as in this tutorial

Start with @BeforeMethod for the browser. Sharing one browser across a class looks faster, but one test's leftover state, such as a logged-in session, can then decide whether the next test passes.

Step 3: Run the test with Maven

Open a terminal in the project folder and run:

bash
mvn test

The first run takes longer, because Maven downloads Selenium and TestNG and Selenium Manager downloads the browser driver. This is the test section of our Chrome run, with the Java stack trace and Maven's blank lines removed and nothing else changed:

[INFO] Running io.qable.firsttest.LoginTest
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Browser: chrome 155.0.8059.85
Browser: chrome 155.0.8059.85
Browser: chrome 155.0.8059.85
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 13.24 s <<< FAILURE! -- in io.qable.firsttest.LoginTest
[ERROR] io.qable.firsttest.LoginTest.flashMessageMatchesExactly -- Time elapsed: 0.673 s <<< FAILURE!
java.lang.AssertionError: 
expected [You logged into a secure area!] but found [You logged into a secure area!
×]
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE

The three SLF4J warnings are harmless. They mean Selenium found no logging library, so its own log messages are switched off.

To run the same tests in Firefox, pass the browser property:

bash
mvn test -Dbrowser=firefox

Firefox gave identical results: 3 tests run, 1 failure, 20.9 seconds in total. On our very first Firefox attempt, though, the browser refused to start, and every test was skipped with a SessionNotCreatedException. We changed nothing and ran it again. It started, and it started on every run after that. If a session will not start, re-run once before you change your code.

Run one test or one class

While you write a test, you rarely want the whole suite. Surefire's test property picks a class, or a single method after a hash sign:

bash
mvn test -Dtest=LoginTest#validLoginOpensTheSecureArea

Our run of that command opened one browser and ran one test:

[INFO] Running io.qable.firsttest.LoginTest
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Browser: chrome 155.0.8059.85
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.266 s -- in io.qable.firsttest.LoginTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Use -Dtest=LoginTest for the whole class. The same property works in a CI job, which makes it the quickest way to re-run a test that failed overnight.

Figure 3 · One run, method by method
The browser start costs more than the test
Each test gets a fresh browser in @BeforeMethod and closes it in @AfterMethod.
flashMessageMatchesExactly
Failed
@BeforeMethod 4,256 ms · @Test 668 ms · @AfterMethod 128 ms
validLoginOpensTheSecureArea
Passed
@BeforeMethod 2,973 ms · @Test 673 ms · @AfterMethod 105 ms
wrongPasswordShowsAnError
Passed
@BeforeMethod 3,178 ms · @Test 693 ms · @AfterMethod 101 ms
Opening the browser took 81% of the time. TestNG ran the three tests in alphabetical order.
Source: testng-results.xml from our final Chrome 155 run, TestNG 7.12.0, 28 October 2026.
Figure 4 · Our runs
Same tests, four runs, one false alarm
One test fails on purpose. The Firefox start-up failure was the browser, not the test.
Chrome 155, Surefire 3.5.6
2 passed, 1 failed
15.9 s in total
Chrome 155, Surefire 3.6.1
2 passed, 1 failed
the same tests on the newest Surefire
Firefox 159, first attempt
Could not start
SessionNotCreatedException; not seen again in 3 re-runs
Firefox 159, final run
2 passed, 1 failed
20.9 s in total
Source: Maven output from our runs, 28 October 2026. We did not establish why Firefox failed to start the first time.

Step 4: Read the failure

TestNG reports a failed assertion with both values. This is what our failing test printed:

java.lang.AssertionError: 
expected [You logged into a secure area!] but found [You logged into a secure area!
×]

The two values look the same, but the found value has a second line. The flash message on the page contains a close link, shown as a multiplication sign, and getText() returns all the visible text inside the element, the link included. An exact comparison therefore fails, even though the login worked.

This is the most useful lesson in a first test. The page was right and the assertion was wrong. You have two good fixes: check for the words with contains(), as the first test does, or read the text from a narrower element.

Fix the failing test

The login page gives the message and the close link no separate elements, so here the fix is to compare only the first line of the text:

    @Test
    public void flashMessageFirstLineMatchesExactly() {
        driver.get("https://the-internet.herokuapp.com/login");
        driver.findElement(By.id("username")).sendKeys("tomsmith");
        driver.findElement(By.id("password")).sendKeys("SuperSecretPassword!");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        // The close link adds a second line; compare only the message line.
        String message = driver.findElement(By.id("flash")).getText().lines().findFirst().orElse("");
        Assert.assertEquals(message, "You logged into a secure area!");
    }

This version passed in both of our runs. It still checks the exact wording, which a contains() check does not, so a typo in the message would still fail the test.

Whichever fix you choose, prove the test can still fail. Put a deliberate typo in the expected text, run it once, see it go red, then remove the typo. A test that cannot fail protects nothing.

Figure 5 · Reading a failure
The page was right, the assertion was wrong
TestNG prints both values, so the cause is visible in the report.
Expected
You logged into a secure area!
Found
You logged into a secure area!
×
The extra line is the close link inside the same element. getText() returns all visible text, so an exact match fails. Check for the words with contains(), or read the text of a narrower element.
Source: Maven output from our Chrome and Firefox runs, 28 October 2026, LoginTest.java line 65.

Step 5: Find the test reports

When Maven runs TestNG, the reports go to target/surefire-reports. Open emailable-report.html in a browser for a one-page summary, or index.html for the full report.

testng-failed.xml is the file to remember. It is a ready-made suite that re-runs only the tests that failed.

Re-run only the tests that failed

This is the testng-failed.xml TestNG wrote after our Chrome run:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Failed suite [Surefire suite]" verbose="0">
  <test thread-count="5" name="Surefire test(failed)" verbose="0">
    <classes>
      <class name="io.qable.firsttest.LoginTest">
        <methods>
          <include name="flashMessageMatchesExactly"/>
          <include name="closeBrowser"/>
          <include name="openBrowser"/>
        </methods>
      </class> <!-- io.qable.firsttest.LoginTest -->
    </classes>
  </test> <!-- Surefire test(failed) -->
</suite> <!-- Failed suite [Surefire suite] -->

It lists the failed test and the setup and teardown methods that test needs, so the browser still opens and closes around it. To use it, copy it out of target, which Maven clears on the next build, and pass it to Surefire:

bash
mvn test -Dsurefire.suiteXmlFiles=testng-failed.xml

Our run of that command ran one test instead of three:

[INFO] Running TestSuite
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Browser: chrome 155.0.8059.85
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 5.716 s <<< FAILURE! -- in TestSuite
[ERROR] io.qable.firsttest.LoginTest.flashMessageMatchesExactly -- Time elapsed: 0.832 s <<< FAILURE!
java.lang.AssertionError: 
expected [You logged into a secure area!] but found [You logged into a secure area!
×]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE

The test failed again, because we had not fixed it yet. That is the point: once you have a fix, this file lets you confirm it in seconds, without waiting for the whole suite. Run the full suite once more before you merge, because a fix for one test can break another.

Once you have more than one TestNG test case, you can also write your own testng.xml to decide which tests run together.

Figure 6 · Where the results go
Six files in target/surefire-reports
Run with Maven, TestNG's reports land in Surefire's folder, not in test-output.
emailable-report.html
One-page summary to read or attach to a ticket
index.html
The full TestNG report, test by test
testng-results.xml
Machine-readable results, with each method's status and duration
testng-failed.xml
A ready-made suite that re-runs only what failed
TEST-io.qable.firsttest.LoginTest.xml
JUnit-format results most CI servers read
io.qable.firsttest.LoginTest.txt
Plain-text summary with the failure message
Source: target/surefire-reports after our run, Maven Surefire 3.5.6 and TestNG 7.12.0.

Running the same test in Eclipse

You can run everything above without an IDE. If you prefer Eclipse, install the TestNG plugin from the Eclipse Marketplace first. The screenshots below come from the first version of this tutorial. The dialogs are the same in current Eclipse, but the versions and package names in them are from 2016.

The simplest route is to import the Maven project, with File, then Import, then Existing Maven Projects. Your pom.xml brings in Selenium and TestNG, so you can skip the first two steps below.

Create a Java project in Eclipse

Create a new Java project
Create a new Java project

Without Maven, you start from File, then New, then Java Project, and give the project a name.

Add the TestNG library to the build path

Add the library to your Java project
Add the library to your Java project

Right-click the project and choose Build Path, then Add Libraries, then TestNG. This adds TestNG only. You would still need to add Selenium yourself, which is why Maven is the better route.

Add a TestNG class

Adding a TestNG class
Adding a TestNG class

Right-click your package and choose New, then Other, then TestNG class.

Choose @BeforeMethod and @AfterMethod

TestNG class setup
TestNG class setup

In the dialog, name the class and tick the @BeforeMethod and @AfterMethod boxes. Eclipse generates an empty class with those methods, which you can fill in with the code from step 2.

Run as TestNG Test

Run As TestNG Test Case
Run As TestNG Test Case

Right-click the class or the project and choose Run As, then TestNG Test. Results appear in the TestNG view at the bottom of the window.

Open the report in test-output

View the TestNG Test Case Report
View the TestNG Test Case Report

A run from Eclipse writes its report to a test-output folder, which the TestNG documentation gives as the default report directory. Right-click the HTML report and open it with the web browser.

In IntelliJ IDEA, add TestNG as a Maven dependency and the IDE runs TestNG tests from the green arrow next to each class and method.

Mistakes to avoid in your first TestNG test

The first version of this tutorial created the driver as a field: WebDriver driver = new FirefoxDriver();. It then closed that driver in @AfterMethod. That works for exactly one test. We copied the pattern into a class with two tests and ran it:

[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 2, Time elapsed: 4.982 s <<< FAILURE! -- in io.qable.firsttest.FieldInitTest
[ERROR] io.qable.firsttest.FieldInitTest.open -- Time elapsed: 0.319 s <<< FAILURE!
org.openqa.selenium.NoSuchSessionException: 
Session ID is null. Using WebDriver after calling quit()?

Surefire counts the setup and teardown methods too, so it reports 4 run and 2 skipped. TestNG's own report shows the two tests: 1 passed and 1 skipped.

The first test passed and closed the browser. TestNG reuses the same class instance for every test, so @BeforeMethod for the second test called a driver that had already quit. TestNG then skipped the second test. Create the driver inside @BeforeMethod, as step 2 does, and every test gets a fresh browser.

The earlier version also paused with Thread.sleep(1000). A fixed pause slows every run and still fails when the page is slower than the pause. Our login tests need no pauses: Selenium waits for each page to finish loading after get() and after the login click, so each element is already there when findElement looks for it.

Waiting for content that loads later

Content that appears after the page has loaded is different. The practice site's dynamic loading page shows "Hello World!" a few seconds after you press Start. We wrote two tests against it: one that looks straight away, and one that uses WebDriverWait.

    @Test
    public void waitForContentThatLoadsLater() {
        driver.get("https://the-internet.herokuapp.com/dynamic_loading/2");
        driver.findElement(By.cssSelector("#start button")).click();

        long start = System.nanoTime();
        WebElement finish = new WebDriverWait(driver, Duration.ofSeconds(10))
                .until(ExpectedConditions.visibilityOfElementLocated(By.id("finish")));
        System.out.println("Waited " + (System.nanoTime() - start) / 1_000_000 + " ms");

        Assert.assertEquals(finish.getText(), "Hello World!");
    }

    @Test
    public void lookingTooSoonFails() {
        driver.get("https://the-internet.herokuapp.com/dynamic_loading/2");
        driver.findElement(By.cssSelector("#start button")).click();

        // No wait: the element does not exist yet, so this throws NoSuchElementException.
        driver.findElement(By.id("finish"));
    }

The test without a wait failed with NoSuchElementException in both of our runs. The test with a wait passed. This is the line it printed, then the other test's failure, from our first run:

Waited 5176 ms
[ERROR] io.qable.firsttest.NextStepsTest.lookingTooSoonFails -- Time elapsed: 3.362 s <<< FAILURE!
org.openqa.selenium.NoSuchElementException: 
no such element: Unable to locate element: {"method":"css selector","selector":"#finish"}

The wait returned as soon as the text appeared, about five seconds after the click. Thread.sleep(1000) would have failed here, and Thread.sleep(10000) would have wasted five seconds on every run. Set the wait's limit to the longest delay you will accept, and let it return early.

Figure 7 · First-test mistakes
Six mistakes, each with its symptom
Most of these appeared in our runs or in this post's earlier version.
Creating the driver as a field
The second test is skipped: "Session ID is null. Using WebDriver after calling quit()?"
Setting a driver path by hand
Breaks when the browser updates itself; Selenium Manager does this for you.
Debugging a start-up failure first
A session that will not start can be the machine. Re-run once before you change code.
Thread.sleep() instead of a wait
Slows every run and still fails when the page is slower than the pause.
Exact-matching visible text
Fails on extras you did not notice, such as a close link's character.
Relying on test order
TestNG ran our tests alphabetically; a test must not depend on another test's data.
Source: our runs, 28 October 2026, including a copy of the earlier version's field-created driver.

A note on Maven Surefire versions

Maven Surefire 3.6 changed how it runs TestNG. On our runs, 3.6.1 handled this tutorial's project without a problem, because the project has no suite file. Once you add a testng.xml and list it in the Surefire configuration, 3.6.1 stops the build with an error. Our TestNG framework guide shows the error and the fix.

That is why the pom.xml above pins 3.5.6. It costs nothing now and saves you a confusing failure later. When you outgrow this pin, you can pass a different version on the command line, as we did: mvn test -Dsurefire.version=3.6.1.

Figure 8 · Maven Surefire
The newest Surefire is fine until you add a suite file
3.5.6 works in both cases, which is why this tutorial pins it.
No testng.xml (this tutorial)
Surefire 3.5.6
Runs
Surefire 3.6.1
Runs
suiteXmlFiles in pom.xml
Surefire 3.5.6
Runs
Surefire 3.6.1
Build stops with an error
Source: our runs of this project on both versions, 28 October 2026; the suite-file error is from our TestNG framework guide's run.
From our work
For EigenRisk, an insurtech firm whose EigenPrism platform runs real-time catastrophe risk analytics, QAble started with no automation at all. The case study reports 900+ automated test cases built and maintained, cross-browser coverage across Chrome, Firefox and Safari, and daily QA governance.
Suites that size are built from the habits in this tutorial. One rule we apply on every engagement: each test opens its own browser and leaves nothing behind for the next one. That is an observation from practice, not a measured figure, and the field-created driver above shows what breaks without it.
Source: QAble's EigenRisk case study, qable.io/casestudy/eigenrisk.

Where this test sits among test levels

The ISTQB Certified Tester Foundation Level syllabus, version 4.0.1, separates tests by level. It describes component testing, "also known as unit testing", as testing "components in isolation", and notes that it "often requires specific support, such as test harnesses or unit test frameworks". System testing, by contrast, "focuses on the overall behavior and capabilities of an entire system or product, often including functional testing of end-to-end tasks".

The login test in this tutorial is a system-level test: it drives a real browser against a deployed site. TestNG's own site says it is "designed to cover all categories of tests: unit, functional, end-to-end, integration", so one framework runs both kinds. Keep the levels in separate classes or groups. Fast component tests can then run on every commit, and browser tests can run where the extra time is worth it.

Where to go next

With one class working, the next steps are the features TestNG is known for:

  • Groups: tag tests as smoke or regression and run one group at a time.
  • Data providers: run one test with many sets of input data.
  • Parallel runs: run tests on several threads to cut suite time.
  • testng.xml: decide which classes and groups run together.

Our TestNG framework guide covers each of these with tested code. Keep the habits from this tutorial as the suite grows: one browser per test, assertions that check what matters, and versions pinned in pom.xml.

If you want help building a TestNG and Selenium suite that stays reliable as it grows, talk to QAble's automation testing services team.

Frequently Asked Questions

What is TestNG?

TestNG is an open-source testing framework for Java, created by Cédric Beust. It runs your tests, gives you setup and teardown methods, assertions, groups, data providers and parallel runs, and writes HTML and XML reports. Selenium drives the browser; TestNG decides what runs and records the results.

How do I run a TestNG test in a Maven project?

Add the TestNG dependency and the Maven Surefire plugin to pom.xml, put your test classes under src/test/java, and run mvn test from the project folder. Surefire detects TestNG and runs every class it finds. Our run used TestNG 7.12.0 with Surefire 3.5.6, and 3.6.1 gave the same results.

How do I run a test using the mvn command?

mvn test runs every test in the project. To run one class, use mvn test -Dtest=LoginTest, and to run one method, use mvn test -Dtest=LoginTest#validLoginOpensTheSecureArea. Any other -D value, such as -Dbrowser=firefox, reaches your code through System.getProperty.

What is the difference between @BeforeMethod and @BeforeTest in TestNG?

@BeforeMethod runs before every test method, so it is the place to open a fresh browser. @BeforeTest runs once before each test tag in testng.xml, which can cover many classes and methods. Use @BeforeMethod for per-test setup, as in this tutorial, so no test inherits another test's browser.

Do I still need to download ChromeDriver or GeckoDriver?

No. Selenium Manager has shipped with every Selenium release since version 4.6, and it finds or downloads the right driver for your installed browser. You only need to manage drivers yourself if your network blocks the download, in which case Selenium's documentation describes how to point it at a mirror.

Which Java version does TestNG need?

TestNG's README states that it requires Java 11 or higher. Selenium 4.51.0's classes are compiled for Java 11, so it needs Java 11 or later too. We compiled for Java 17 and ran on JDK 21.

How do I run Selenium tests in parallel with TestNG?

Set the parallel attribute in testng.xml to methods, classes or tests, and set thread-count. Each test must create its own driver, as the @BeforeMethod in this tutorial does, or threads will share one browser. Our TestNG framework guide shows a parallel run with timings.

Is TestNG a BDD framework?

No. TestNG is a test runner in which you write tests as Java methods. Behaviour-driven development tools such as Cucumber describe tests in plain-language scenarios, and Cucumber can use TestNG to run them.

Why does my TestNG test fail with "Session ID is null"?

The driver was closed before the test used it. It usually happens when the driver is created once, as a field, and closed in @AfterMethod: the second test then calls a browser that has already quit. Create the driver inside @BeforeMethod instead.

Where does TestNG save its reports?

Run through Maven, reports go to target/surefire-reports, including emailable-report.html, index.html and testng-failed.xml. Run from the Eclipse plugin or the command line without Maven, they go to test-output, which the TestNG documentation gives as the default.

Free Assessment

Get a free QA audit for your project

Identify quality gaps before they become production bugs.

Get Free Audit

Ship software with confidence

Talk to a QA advisor and find out how QAble can help your team build quality in at every stage.

No sales pitch
Technical walkthrough
No lock-in commitment

Talk to QA Advisor

Direct access to QAble's QA specialists.

Response within 24 hours