Exit course
Chapter 3 of 11 Intermediate 7 min read 3 quick checks Updated

Your first Selenium test with TestNG

On this page
  1. The test class
  2. What each part does
  3. Explicit waits
  4. Headless runs and window size
  5. Reading a failure
  6. Running a single test

Time to drive a real browser. We will test the web form that the Selenium project publishes for exactly this purpose, at https://www.selenium.dev/selenium/web/web-form.html, and a second page that adds elements after a delay, so we can see why waiting matters.

The test class#

Replace FirstTest.java with:

package com.example;

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class FirstTest {

    private WebDriver driver;
    private WebDriverWait wait;

    @BeforeMethod
    public void startBrowser() {
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @Test
    public void submitsTheWebForm() {
        driver.get("https://www.selenium.dev/selenium/web/web-form.html");
        Assert.assertEquals(driver.getTitle(), "Web form");

        driver.findElement(By.id("my-text-id")).sendKeys("Selenium");
        driver.findElement(By.name("my-password")).sendKeys("secret");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        WebElement message = wait.until(
                ExpectedConditions.visibilityOfElementLocated(By.id("message")));
        Assert.assertEquals(message.getText(), "Received!");
    }

    @Test
    public void waitsForADelayedElement() {
        driver.get("https://www.selenium.dev/selenium/web/dynamic.html");
        driver.findElement(By.id("adder")).click();

        // The page adds the box after a 1 second setTimeout
        WebElement box = wait.until(
                ExpectedConditions.presenceOfElementLocated(By.id("box0")));
        Assert.assertEquals(box.getDomAttribute("class"), "redbox");
    }

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

Run it with mvn test. Chrome opens twice (once per test method), and you should see Tests run: 2, Failures: 0.

What each part does#

@BeforeMethod / @AfterMethod run around every @Test method, so each test gets a fresh browser with no cookies or state left over from the previous one. alwaysRun = true on the teardown means it also runs when the test fails, which is what prevents orphaned Chrome processes piling up on your machine. Chapter 3 looks at the full set of lifecycle annotations.

driver.get(url) navigates and blocks until the page's load event fires. It does not wait for JavaScript that runs afterwards, such as content rendered by a framework or fetched with fetch().

Locators. By.id, By.name and By.cssSelector are the ones you will use most. Prefer IDs, names or dedicated test attributes ([data-testid='submit']) over long CSS chains or XPath that mirror the page layout: layout changes far more often than IDs do. Once a page is used by more than one test, wrap its locators in a page object; the Page Object Model course covers that pattern, so we will keep locators inline here.

findElement vs findElements. findElement throws NoSuchElementException when nothing matches. findElements returns an empty list instead, which is the right tool for asserting that something is absent.

You need to assert that an error banner is not on the page. Which approach fits?

findElements returns an empty list when nothing matches. findElement never returns null; it throws, so asserting null on it cannot pass.

Explicit waits#

The second test is the important one. The dynamic.html page appends #box0 one second after you click the button. Without a wait, findElement(By.id("box0")) runs immediately, finds nothing and throws.

WebDriverWait polls a condition until it returns something non-null (or true), or the timeout expires, in which case it throws TimeoutException. The ExpectedConditions class has ready-made conditions:

Condition Returns when
presenceOfElementLocated(by) the element is in the DOM (may be hidden)
visibilityOfElementLocated(by) the element is in the DOM and displayed
elementToBeClickable(by) visible and enabled
textToBePresentInElementLocated(by, text) the element's text contains text
invisibilityOfElementLocated(by) the element is gone or hidden (spinners)
urlContains(fragment) the current URL contains fragment

You can also pass a lambda when none of them fit:

String status = wait.until(d -> {
    String text = d.findElement(By.id("status")).getText();
    return text.isEmpty() ? null : text;   // null means "keep polling"
});

Three rules keep waits from becoming a source of flakiness:

  1. Wait for the condition the next line depends on, not for a duration. Thread.sleep(2000) is too long on a fast machine and too short on a loaded CI agent.
  2. Don't mix implicit and explicit waits. An implicit wait (driver.manage().timeouts().implicitlyWait(...)) applies to every findElement call, including the ones inside WebDriverWait. The Selenium docs warn that combining a 10-second implicit wait with a 15-second explicit wait can time out after 20 seconds. Pick explicit waits and leave the implicit wait at its default of 0.
  3. Don't "fix" an intermittent failure by raising the timeout. Find out which condition was not yet true and wait for that instead.

The suite sets a 10 second implicit wait and also uses 15 second explicit waits. Some failures take more than 20 seconds to report. What is the fix?

The implicit wait applies to every findElement, including those inside WebDriverWait, so the two combine unpredictably. Pick explicit waits.

Headless runs and window size#

On a CI server you will usually run Chrome headless:

import org.openqa.selenium.chrome.ChromeOptions;

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");
driver = new ChromeDriver(options);

Set the window size explicitly. When we ran this exact form test headless without it, the default headless window was small enough that another element overlapped the submit button, and the click failed with ElementClickInterceptedException. A headed browser on a large monitor never shows the problem, which makes it a classic "passes locally, fails in CI" bug. If you do hit that exception, this guide to ElementClickInterceptedException walks through the other causes.

A test passes in a headed browser on your monitor but fails on CI in headless Chrome with ElementClickInterceptedException. What should you check first?

The default headless window is small, so other elements can overlap the target. A fixed window size makes local and CI layouts match.

Reading a failure#

Change the expected title to "Web form!" and run again. TestNG reports:

java.lang.AssertionError: expected [Web form!] but found [Web form]

The message format is expected [<second argument>] but found [<first argument>]. In TestNG, assertEquals(actual, expected) takes the actual value first, the opposite of JUnit. Swap them and your failure messages will lie to you; chapter 4 has more on assertions.

Exceptions thrown by Selenium itself (NoSuchElementException, TimeoutException, StaleElementReferenceException) also fail the test, with the locator in the message. Our article on exception handling with Selenium explains what each one usually means.

Running a single test#

While you are working on one test, run just that method:

mvn test -Dtest=FirstTest#waitsForADelayedElement

or use the green arrow next to the method in IntelliJ IDEA or Eclipse (with the TestNG plugin).

Our test class now has its setup and teardown copied into it. Add a second class and you would copy them again. The next chapter moves that lifecycle into a base class and explains where each TestNG annotation fits.