Skip to content
SDETRole
SDETRole

Lesson 8 of 9 · Programming for Testers

Java for Testers

The Java 21 you need for Selenium, REST Assured, JUnit 5 and TestNG: project layout, types, strings, collections, streams, records, exceptions and a first data-driven test.

intermediate 8 min read Updated

You will learn to

  • Set up a Maven test project and understand the standard directory layout
  • Use Java types, strings, collections, streams and records in test code
  • Write JUnit 5 and TestNG tests, including parameterised tests
  • Write a basic Selenium 4 test with an explicit wait

Prerequisites

  • Programming Fundamentals for Testers
  • OOP for Test Automation

Java is still the most common language for Selenium and REST Assured frameworks, especially in large enterprises and in many SDET job descriptions. If you already know JavaScript or Python, Java will feel stricter: everything has a declared type, everything lives in a class, and the compiler rejects mistakes before anything runs. That strictness is useful in large test suites, where it catches whole categories of errors early.

This lesson covers the Java 21 features you will use in automation, assuming you know general programming concepts from Programming Fundamentals for Testers and OOP from OOP for Test Automation.

Setting up a project

Most Java test projects use Maven or Gradle. Maven’s standard layout:

my-tests/
  pom.xml
  src/
    main/java/         (framework code, if you separate it)
    test/java/         (test classes)
      com/example/tests/LoginTest.java
    test/resources/    (test data, config files)

Dependencies are declared in pom.xml. A minimal setup for JUnit 5 and Selenium:

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <junit.version>5.11.4</junit.version>
  <selenium.version>4.27.0</selenium.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>${selenium.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>

The versions shown are examples; use the latest releases from Maven Central for your project. Run tests with mvn test, which uses the Surefire plugin; make sure your pom.xml uses a recent Surefire version so JUnit 5 tests are discovered.

Types and variables

Java is statically typed. Primitive types hold simple values; everything else is an object.

Type Example Notes
int, long int retries = 3; Whole numbers; long for large values like timestamps
double double price = 19.99; Floating point; use BigDecimal for money calculations
boolean boolean visible = true;
char char grade = 'A'; Single character, single quotes
String String email = "a@example.com"; An object, immutable

var (Java 10+) infers the type of a local variable from its initialiser:

var users = new ArrayList<String>(); // type is ArrayList<String>
var timeout = Duration.ofSeconds(10);

It is still static typing; the type is fixed at compile time.

Wrapper types and null

Collections cannot hold primitives, so Java uses wrapper classes: Integer, Long, Double, Boolean. These can be null, and unboxing a null wrapper to a primitive throws NullPointerException, a classic bug when reading optional values from maps or JSON.

Strings

String name = "Ana";
String greeting = "Hello, " + name;            // concatenation
String formatted = "Order %s total %.2f".formatted("o-1", 25.5); // Java 15+

boolean same = name.equals("Ana");             // compare content
boolean wrong = name == "Ana";                 // compares references: do not use

Text blocks (Java 15+) are ideal for JSON payloads in API tests:

String body = """
    {
      "name": "Ana",
      "email": "ana@example.com"
    }
    """;

Other everyday methods: trim() and strip(), contains, startsWith, split, replace, toLowerCase, isBlank().

Collections

The collections framework lives in java.util. Program to the interface type (List, Map, Set) and choose the implementation on the right:

List<String> browsers = new ArrayList<>(List.of("chrome", "firefox"));
browsers.add("edge");

Map<String, Integer> statusCounts = new HashMap<>();
statusCounts.merge("200", 1, Integer::sum);   // increment a counter

Set<String> seenIds = new HashSet<>();
boolean isNew = seenIds.add("o-1");            // false if already present

List.of, Set.of and Map.of create immutable collections; adding to them throws UnsupportedOperationException. See Data Structures You Will Use for choosing between them.

Streams

Streams express transformations over collections, similar to JavaScript’s map and filter:

record Product(String name, double price, boolean inStock) {}

List<Product> products = List.of(
    new Product("Kettle", 39.99, true),
    new Product("Toaster", 24.50, false),
    new Product("Blender", 59.00, true)
);

List<String> available = products.stream()
    .filter(Product::inStock)
    .map(Product::name)
    .toList();                                  // Java 16+: [Kettle, Blender]

boolean allPriced = products.stream().allMatch(p -> p.price() > 0);
double total = products.stream().mapToDouble(Product::price).sum();

In Selenium, streams are handy for reading lists of elements:

List<String> names = driver.findElements(By.cssSelector("[data-testid=product-name]"))
    .stream()
    .map(WebElement::getText)
    .toList();

Records and modern syntax

Records (Java 16+) are concise, immutable data classes, perfect for test data and expected API responses:

public record User(String name, String email, String role) {}

User admin = new User("Ana", "ana@example.com", "admin");
admin.email();                       // accessor, no "get" prefix
admin.equals(new User("Ana", "ana@example.com", "admin")); // true: value equality

Switch expressions (Java 14+) return a value and do not fall through:

WebDriver driver = switch (browser) {
    case "firefox" -> new FirefoxDriver();
    case "edge" -> new EdgeDriver();
    default -> new ChromeDriver();
};

Exceptions in brief

Java has checked exceptions (must be caught or declared with throws, such as IOException) and unchecked ones (subclasses of RuntimeException, including all Selenium exceptions). Failed assertions throw AssertionError. Use try-with-resources for anything closeable. The details are in Exception Handling.

JUnit 5

package com.example.tests;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class PriceCalculatorTest {

    private PriceCalculator calc;

    @BeforeEach
    void setUp() {
        calc = new PriceCalculator();
    }

    @Test
    @DisplayName("10% discount is applied to the subtotal")
    void appliesPercentageDiscount() {
        assertEquals(90.0, calc.applyDiscount(100.0, 10), 0.001);
    }

    @ParameterizedTest(name = "qty {0} is valid: {1}")
    @CsvSource({ "0, false", "1, true", "99, true", "100, false" })
    void validatesQuantity(int qty, boolean expected) {
        assertEquals(expected, calc.isValidQuantity(qty));
    }

    @Test
    void groupsRelatedChecks() {
        assertAll(
            () -> assertTrue(calc.isValidQuantity(1)),
            () -> assertFalse(calc.isValidQuantity(-1))
        );
    }
}

Key annotations: @Test, @BeforeEach/@AfterEach, @BeforeAll/@AfterAll (static methods by default), @DisplayName, @Tag, @Disabled, @ParameterizedTest with sources such as @ValueSource, @CsvSource and @MethodSource. assertAll reports every failing check in the group instead of stopping at the first. The PriceCalculator class is a stand-in for whatever you are testing.

TestNG

TestNG is common in older and enterprise Selenium frameworks, largely for its grouping, dependencies and XML suite files.

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

public class LoginTest {

    private LoginPage loginPage; // your page object, created in a @BeforeMethod

    @DataProvider(name = "invalidLogins")
    public Object[][] invalidLogins() {
        return new Object[][] {
            { "", "secret", "Email is required" },
            { "ana@example.com", "", "Password is required" },
        };
    }

    @Test(dataProvider = "invalidLogins", groups = "regression")
    public void showsValidationMessage(String email, String password, String expected) {
        loginPage.signIn(email, password);
        String actual = loginPage.errorMessage();
        Assert.assertEquals(actual, expected); // TestNG: actual first
    }
}

Watch the argument order: TestNG’s Assert.assertEquals(actual, expected) takes actual first, while JUnit’s assertEquals(expected, actual) takes expected first. Swapping them produces misleading failure messages.

A first Selenium test

Selenium 4.6+ includes Selenium Manager, which downloads a matching browser driver automatically, so you no longer need to manage chromedriver yourself.

import java.time.Duration;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

class SearchTest {
    private WebDriver driver;

    @BeforeEach
    void start() {
        driver = new ChromeDriver();
    }

    @AfterEach
    void stop() {
        driver.quit();
    }

    @Test
    void searchShowsResults() {
        driver.get("https://shop.example.test");
        driver.findElement(By.name("q")).sendKeys("kettle", Keys.ENTER);

        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        WebElement heading = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.cssSelector("h1"))
        );
        assertEquals("Results for \"kettle\"", heading.getText());
    }
}

The URL is a placeholder. Use explicit waits like WebDriverWait rather than Thread.sleep, and avoid mixing implicit and explicit waits, which the Selenium documentation warns can cause unpredictable wait times. Practise locators and waits in the Selenium Lab.

Java vs JavaScript, quick reference

Concept Java JavaScript / TypeScript
Typing Static, compiled Dynamic (JS) or static at compile time (TS)
Equality of content a.equals(b) === for primitives; deep-equal helpers for objects
List List<String> string[]
Map Map<String, Integer> Map or object
Lambda x -> x * 2 (x) => x * 2
Waiting on I/O Blocking calls Promises with await
Test runners JUnit 5, TestNG Playwright Test, Jest, Vitest

Practice exercise

Write a JUnit 5 test class for a PasswordPolicy class with a method boolean isValid(String password). Rules: at least 12 characters, at least one digit, and not equal to the username “admin” in any case.

  1. Implement PasswordPolicy (a few lines).
  2. Write a @ParameterizedTest with @CsvSource covering boundary lengths and each rule.
  3. Use a record to hold a test case if you choose @MethodSource instead.
Show a model answer
public class PasswordPolicy {
    public boolean isValid(String password) {
        if (password == null || password.length() < 12) return false;
        if (password.chars().noneMatch(Character::isDigit)) return false;
        return !password.equalsIgnoreCase("admin");
    }
}
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class PasswordPolicyTest {

    private final PasswordPolicy policy = new PasswordPolicy();

    @ParameterizedTest(name = "\"{0}\" valid={1}")
    @CsvSource({
        "abcdefghij1,   false",   // 11 chars: just below the minimum
        "abcdefghijk1,  true",    // 12 chars: exactly the minimum
        "abcdefghijkl,  false",   // 12 chars but no digit
        "abcdefghijklm1, true",   // 14 chars with digit
        "'',            false"    // empty string
    })
    void checksPolicy(String password, boolean expected) {
        assertEquals(expected, policy.isValid(password));
    }
}

The “admin” rule cannot be reached once the length rule applies (admin is 5 characters), which is worth pointing out: the rule as written is redundant, and a good tester would ask whether it should instead reject passwords containing the username. @CsvSource trims whitespace around values by default, and '' represents an empty string.

Key takeaways

  • Java test projects use Maven or Gradle with tests under src/test/java; Selenium Manager handles browser drivers.
  • Compare strings with equals, use BigDecimal for money, and beware of null wrapper types.
  • Collections and streams handle most test data work; List.of and friends are immutable.
  • Records, text blocks, var and switch expressions make modern Java test code much shorter.
  • Know both JUnit 5 and TestNG, including the opposite assertEquals argument order.

Key terms

Finished this lesson?

Progress is saved in this browser. No account needed.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →