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.
- Implement
PasswordPolicy(a few lines). - Write a
@ParameterizedTestwith@CsvSourcecovering boundary lengths and each rule. - Use a record to hold a test case if you choose
@MethodSourceinstead.
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, useBigDecimalfor money, and beware ofnullwrapper types. - Collections and streams handle most test data work;
List.ofand friends are immutable. - Records, text blocks,
varand switch expressions make modern Java test code much shorter. - Know both JUnit 5 and TestNG, including the opposite
assertEqualsargument order.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.
Practise it
Related interview questions
- Which TypeScript utility types do you use most, and how do they help in test code?
- Using Java streams, find the duplicate values in a list and count their occurrences.
- Why are Strings immutable in Java, and when should you use StringBuilder?
- Explain the equals() and hashCode() contract. What breaks if you violate it?