Mastering JUnit 5: Annotations, Lifecycle, and Test Suites
JUnit 5 Architecture
JUnit 5 represents a major evolution in the JUnit framework, designed for Java 8 and above. It is composed of three distinct modules: JUnit Platform, JUnit Jupiter, and JUnit Vintage.
- JUnit Platform: Serves as the foundation for launching testing frameworks on the JVM. It provides a stable API for tools like IDEs and build tools (Maven, Gradle) to discover and execute tests.
- JUnit Jupiter: The core module combining the new programming model and extension model. It contains the annotations and APIs used to write modern JUnit 5 tests.
- JUnit Vintage: Provides backward compatibility, allowing the execution of tests written in JUnit 3 and JUnit 4 within the JUnit 5 environment.
Dependency Configuration
To utilize JUnit 5 in a Maven project, the essential dependencies include the Jupiter API for writing tests, the engine for running them, and the params dependency for parameterized tests.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>Core Annotations and Lifecycle
The framework provides a robust set of annotations to control the test execution lifecycle.
Basic Test Methods
The @Test annotation marks a method as a test case. Unlike previous versions, it does not require public visibility.
public class BasicTest {
@Test
void verifyAddition() {
System.out.println("Executing addition test");
}
@Test
void verifySubtraction() {
System.out.println("Executing subtraction test");
}
}Setup and Teardown
JUnit 5 offers fine-grained control over initialization and cleanup:
@BeforeAll: Executed once before all tests in the class. Must be static.@AfterAll: Executed once after all tests in the class. Must be static.@BeforeEach: Executed before each individual test method.@AfterEach: Executed after each individual test method.
public class LifecycleTest {
@BeforeAll
static void setupGlobal() {
System.out.println("Global initialization");
}
@AfterAll
static void teardownGlobal() {
System.out.println("Global cleanup");
}
@BeforeEach
void setupTest() {
System.out.println("Initializing test data");
}
@AfterEach
void cleanupTest() {
System.out.println("Cleaning test data");
}
@Test
void testCaseOne() {
System.out.println("Test Case 1 logic");
}
}Disabling Tests
The @Disabled annotation can be applied to a class or method to prevent execution.
public class SkipTest {
@Test
@Disabled("Functionality under review")
void incompleteTest() {
System.out.println("This will not run");
}
}Parameterized Tests
Parameterized tests allow a single test method to run multiple times with different arguments.
Using ValueSource
@ValueSource allows passing a simple array of literals (strings, ints, etc.).
public class ParamTest {
@ParameterizedTest
@ValueSource(strings = {"Apple", "Banana", "Cherry"})
void checkFruitNames(String fruit) {
System.out.println("Validating fruit: " + fruit);
assertNotNull(fruit);
}
}Using CsvSource
@CsvSource allows for multiple arguments to be passed as comma-separated values.
public class CsvParamTest {
@ParameterizedTest
@CsvSource({"10, 20, 30", "5, 15, 20"})
void checkSum(int num1, int num2, int expectedSum) {
assertEquals(expectedSum, num1 + num2);
}
}Using MethodSource
For complex data types, @MethodSource references a static factory method that returns a Stream of Arguments.
public class MethodParamTest {
@ParameterizedTest
@MethodSource("userDataProvider")
void testUserCreation(String name, int age) {
System.out.println("User: " + name + ", Age: " + age);
}
static Stream<Arguments> userDataProvider() {
return Stream.of(
Arguments.arguments("Alice", 25),
Arguments.arguments("Bob", 30)
);
}
}Test Execution Order
By default, JUnit 5 does not guarantee the order of test method execution. To enforce a specific sequence, use @TestMethodOrder in conjunction with @Order.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTest {
@Test
@Order(2)
void secondStep() {
System.out.println("Step 2");
}
@Test
@Order(1)
void firstStep() {
System.out.println("Step 1");
}
}Test Instance Lifecycle
The default behavior creates a new instance of the test class for each test method (PER_METHOD). This ensures test isolation. To share state between tests, switch to PER_CLASS lifecycle using @TestInstance.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SharedStateTest {
int sharedCounter = 0;
@Test
@Order(1)
void incrementOnce() {
sharedCounter++;
System.out.println("Counter: " + sharedCounter); // Output: 1
}
@Test
@Order(2)
void incrementAgain() {
sharedCounter++;
System.out.println("Counter: " + sharedCounter); // Output: 2
}
}Assertions
Assertions are the core mechanism to verify expected outcomes. JUnit 5 provides static methods in the org.junit.jupiter.api.Assertions class.
| Method | Description |
|---|---|
assertEquals(expected, actual) | Verifies that two values are equal. |
assertTrue(condition) | Verifies that the condition is true. |
assertNotNull(object) | Verifies that the object is not null. |
assertTimeout(duration, executable) | Verifies that execution completes within the given time. |
public class AssertionTest {
@Test
void validateMath() {
int sum = 5 + 5;
assertEquals(10, sum);
assertTrue(sum > 0);
}
@Test
void checkTimeout() {
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
});
}
}Test Suites
Test suites allow grouping multiple test classes for simultaneous execution. This requires the junit-platform-suite dependency.
@Suite
@SelectClasses({BasicTest.class, LifecycleTest.class})
public class RegressionSuite {
// This class acts as a container for the suite
}Alternatively, packages can be selected using @SelectPackages.
@Suite
@SelectPackages("com.example.tests")
public class PackageSuite {
}When selecting packages, JUnit 5 defaults to scanning for classes following specific naming conventions: ending in Test, starting with Test, ending in IT, or ending in ITCase.