JUnit 5
JUnit 5
Overview
JUnit 5 is the most popular unit testing framework for Java. JUnit 5 (Jupiter) leverages modern Java features and provides an extensible architecture. It supports test-driven development (TDD) and includes advanced features like parameterized tests, dynamic tests, and conditional test execution.
Details
Technical Features
- JUnit Platform: Foundation for launching testing frameworks
- JUnit Jupiter: New programming and extension model
- JUnit Vintage: Backward compatibility for JUnit 3/4 based tests
- Java 8+ Support: Utilizes lambda expressions and Stream API
- Flexible Assertions: Rich assertion methods with detailed error messages
Architectural Evolution
JUnit 5 has evolved significantly from JUnit 4, consisting of three sub-projects. The Jupiter API introduces a new annotation system and allows flexible framework extension through the extension model. It natively supports modern testing approaches like parameterized tests and dynamic tests.
Ecosystem
Deep integration with major Java tools like Spring Boot, Gradle, and Maven, with excellent IDE support. Rich extension libraries and third-party plugins enable various testing scenarios.
Advantages and Disadvantages
Advantages
- Modern Java Features: Maximizes use of Java 8+ features
- Rich Assertions: Expressive and readable test code
- Flexible Extensibility: Easy creation of custom extensions
- Parameterized Tests: Efficient testing with multiple test data
- Conditional Tests: Environment-based test execution control
- Excellent IDE Integration: Native support in major IDEs
- Detailed Error Reporting: Comprehensive failure information for debugging
Disadvantages
- Increased Learning Cost: Learning burden when migrating from JUnit 4
- Configuration Complexity: Setup can become complex when using advanced features
- Library Size: Larger than JUnit 4
- Migration Cost: Work required to migrate existing JUnit 4 test code
Reference Pages
Usage Examples
Hello World (Basic Test)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MyFirstJUnitJupiterTests {
@Test
void myFirstTest() {
// Basic assertions
assertTrue(true);
assertEquals(4, 2 + 2);
assertNotNull("Hello World");
}
@Test
void testWithCustomMessage() {
int expected = 10;
int actual = 5 + 5;
assertEquals(expected, actual, "Calculation result does not match expected value");
}
}
Assertion Examples
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
class AssertionExamples {
@Test
void standardAssertions() {
assertEquals(2, 1 + 1);
assertTrue(1 > 0, "1 is greater than 0");
assertFalse(1 < 0, "1 is not less than 0");
assertNull(null);
assertNotNull("not null");
}
@Test
void groupedAssertions() {
// Grouped assertions
assertAll("person",
() -> assertEquals("John", "John"),
() -> assertEquals("Doe", "Doe")
);
}
@Test
void dependentAssertions() {
// Dependent assertions
assertAll(
() -> {
String firstName = "John";
assertNotNull(firstName);
// Execute only if previous assertion succeeded
assertAll("firstName",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
}
);
}
@Test
void exceptionTesting() {
// Exception testing
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> Integer.parseInt("abc")
);
assertEquals("For input string: \"abc\"", exception.getMessage());
}
@Test
void timeoutTesting() {
// Timeout testing
assertTimeout(Duration.ofMillis(100), () -> {
Thread.sleep(50);
});
}
}
Lifecycle Methods
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LifecycleMethodsTest {
@BeforeAll
static void initAll() {
System.out.println("Executed once before test class starts");
}
@BeforeEach
void init() {
System.out.println("Executed before each test method");
}
@Test
void succeedingTest() {
assertTrue(true, "Succeeding test");
}
@Test
void failingTest() {
// Example of intentionally failing test
// fail("Intentionally failed test");
assertTrue(true, "Actually succeeds");
}
@Test
@Disabled("This test is disabled")
void skippedTest() {
// This test will be skipped
}
@AfterEach
void tearDown() {
System.out.println("Executed after each test method");
}
@AfterAll
static void tearDownAll() {
System.out.println("Executed once after test class ends");
}
}
Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Month;
import java.util.stream.Stream;
class ParameterizedTestExamples {
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8, 13, 21})
void testWithIntValues(int argument) {
assertTrue(argument > 0 && argument < 25);
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@EnumSource(Month.class)
void testWithEnumSource(Month month) {
int quarter = (month.getValue() - 1) / 3 + 1;
assertTrue(quarter >= 1 && quarter <= 4);
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
}
Conditional Tests
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
import static org.junit.jupiter.api.Assertions.*;
class ConditionalTestExamples {
@Test
@EnabledOnOs(OS.MAC)
void onlyOnMacOs() {
// Run only on macOS
assertTrue(System.getProperty("os.name").contains("Mac"));
}
@Test
@EnabledOnOs({OS.LINUX, OS.MAC})
void onLinuxOrMac() {
// Run only on Linux or macOS
assertTrue(true);
}
@Test
@DisabledOnOs(OS.WINDOWS)
void notOnWindows() {
// Run on non-Windows
assertTrue(true);
}
@Test
@EnabledOnJre(JRE.JAVA_11)
void onlyOnJava11() {
// Run only on Java 11
assertTrue(true);
}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// Run only on 64-bit architectures
assertTrue(true);
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// Run only when specific environment variable is set
assertTrue(true);
}
}
Tagging and Filtering
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@Tag("fast")
class TaggedTests {
@Test
@Tag("taxes")
void testingTaxCalculation() {
assertTrue(true);
}
@Test
@Tag("slow")
@Tag("integration")
void testingDatabaseQuery() {
// Database integration test
assertTrue(true);
}
@Test
@Tag("unit")
void fastUnitTest() {
// Fast unit test
assertEquals(4, 2 + 2);
}
}
// Tag filtering configuration example
// junit-platform.properties:
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent
// Gradle tag filtering example:
// test {
// useJUnitPlatform {
// includeTags("fast", "unit")
// excludeTags("slow", "integration")
// }
// }