Overview and History
TestNG (Test Next Generation) is a testing framework designed to simplify a broad range of testing needs, from unit testing to integration testing. Created by Cédric Beust in 2004, TestNG was developed to address limitations in JUnit, which was the predominant testing framework for Java at that time.
The framework was inspired by JUnit but introduced several advanced features that were missing in JUnit 3, such as annotations, flexible test configuration, parameterized testing, and dependency testing. Over the years, TestNG has evolved into a comprehensive testing framework that supports a wide range of testing scenarios and integrates well with various tools and libraries in the Java ecosystem.
TestNG vs JUnit
While both TestNG and JUnit serve as testing frameworks for Java applications, they differ in several key aspects:
Annotations
TestNG provides a richer set of annotations compared to JUnit, offering more granular control over test execution. For example, TestNG includes annotations like @BeforeTest, @AfterTest, @BeforeGroup, and @AfterGroup, which are not available in JUnit.
Test Configuration
TestNG allows for more flexible test configuration through XML files, enabling you to define test suites, groups, and parameters without modifying the test code. JUnit, especially in earlier versions, had more limited configuration options.
Dependencies
TestNG supports dependencies between test methods and groups, allowing you to specify that certain tests should only run if their dependencies pass. This feature is particularly useful for integration testing.
Parameterization
TestNG offers robust support for parameterized testing through @DataProvider and XML configuration, making it easier to run the same test with different data sets.
Parallel Execution
TestNG has built-in support for parallel test execution, which is especially valuable when working with Java 21's virtual threads for improved concurrency.
Setting up TestNG with Maven
To use TestNG in a Maven project, you need to add the TestNG dependency to your pom.xml file:
<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Additionally, you may want to configure the Maven Surefire plugin to work with TestNG:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build>
For Java 21 compatibility, you should also specify the Java version in your Maven configuration:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
TestNG with Java 21
Java 21, released in September 2023, introduces several features that can enhance your testing experience with TestNG:
Virtual Threads
Java 21's virtual threads (Project Loom) provide a lightweight alternative to platform threads, allowing for highly concurrent applications without the overhead of traditional threading. This is particularly useful for TestNG's parallel execution capabilities:
import org.testng.annotations.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadDemo {
@Test
public void testWithVirtualThreads() throws InterruptedException {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
int taskId = i;
executor.submit(() -> {
// Test logic that runs in a virtual thread
System.out.println("Task " + taskId + " running in " + Thread.currentThread());
return null;
});
}
} // executor.close() called implicitly, waits for all tasks
}
}
Record Patterns
Java 21 enhances record patterns, making it easier to destructure and pattern match against record types. This can simplify test data creation and validation:
import org.testng.Assert;
import org.testng.annotations.Test;
public class RecordPatternTest {
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}
@Test
public void testRecordPattern() {
Rectangle rectangle = new Rectangle(new Point(1, 2), new Point(5, 6));
// Using record pattern for destructuring
if (rectangle instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
Assert.assertEquals(x1, 1);
Assert.assertEquals(y1, 2);
Assert.assertEquals(x2, 5);
Assert.assertEquals(y2, 6);
} else {
Assert.fail("Record pattern matching failed");
}
}
}
Pattern Matching for Switch
Java 21's pattern matching for switch expressions allows for more expressive and concise code in test cases:
import org.testng.Assert;
import org.testng.annotations.Test;
public class PatternMatchingSwitchTest {
@Test
public void testPatternMatchingSwitch() {
Object obj = "Hello, TestNG!";
String result = switch (obj) {
case String s when s.length() > 10 -> "Long string: " + s;
case String s -> "Short string: " + s;
case Integer i -> "Integer: " + i;
case null -> "Null value";
default -> "Unknown type";
};
Assert.assertEquals(result, "Long string: Hello, TestNG!");
}
}
Sealed Classes
Java 21's sealed classes can be useful for creating well-defined hierarchies of test data or mock objects:
import org.testng.Assert;
import org.testng.annotations.Test;
public class SealedClassTest {
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
@Test
public void testSealedClassHierarchy() {
Shape shape = new Circle(5.0);
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
Assert.assertEquals(area, Math.PI * 25.0, 0.001);
}
}
Creating Your First TestNG Test with Java 21
Let's create a simple TestNG test for a calculator application using Java 21 features:
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class CalculatorTest {
private Calculator calculator;
@BeforeClass
public void setup() {
calculator = new Calculator();
}
@Test
public void testAddition() {
int result = calculator.add(5, 3);
Assert.assertEquals(result, 8, "Addition operation failed");
}
@Test
public void testSubtraction() {
int result = calculator.subtract(10, 4);
Assert.assertEquals(result, 6, "Subtraction operation failed");
}
@Test
public void testMultiplication() {
int result = calculator.multiply(3, 7);
Assert.assertEquals(result, 21, "Multiplication operation failed");
}
@Test
public void testDivision() {
double result = calculator.divide(10, 2);
Assert.assertEquals(result, 5.0, "Division operation failed");
}
@Test(expectedExceptions = ArithmeticException.class)
public void testDivisionByZero() {
calculator.divide(5, 0);
}
}
// Calculator class
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public double divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return (double) a / b;
}
}
This simple example demonstrates the basic structure of a TestNG test class, including test methods and assertions. In the following chapters, we'll explore more advanced TestNG features and how to leverage Java 21 capabilities to enhance your testing approach.