JUnit 5 — a modern testing framework with support for parameterized tests, extensions, and nested tests.
Simple analogy: JUnit 4 is like an old TV (only on/off). JUnit 5 is like a Smart TV — plays video, connects apps, voice control. JUnit 5 gives you a modular architecture (JUnit Platform + JUnit Jupiter + JUnit Vintage), where you pick exactly the features you need and plug in extensions like apps.
Why it matters: Modern Java projects require testing features beyond simple @Test methods — parameterized inputs, custom lifecycle hooks, dynamic test generation, and test grouping. JUnit 5 provides all of these out of the box, reducing boilerplate and making test suites more expressive and maintainable.
Parameterized tests:
1@ParameterizedTest2@ValueSource(strings = {"racecar", "radar", "level"})3void shouldDetectPalindromes(String word) {4 assertTrue(isPalindrome(word));5}67@ParameterizedTest8@CsvSource({9 "1, 1, 2",10 "2, 3, 5",11 "0, 0, 0"12})13void shouldAdd(int a, int b, int expected) {14 assertEquals(expected, calculator.add(a, b));15}1617@ParameterizedTest18@MethodSource("userProvider")19void shouldValidateUsers(User user) {20 assertTrue(validator.isValid(user));21}2223static Stream<User> userProvider() {24 return Stream.of(25 new User("Alice", "a@b.com"),26 new User("Bob", "b@b.com")27 );28}2930@ParameterizedTest31@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)32void shouldParseCsvData(String input, String expected) {33 assertEquals(expected, parser.parse(input));34}3536@ParameterizedTest37@EnumSource(ChronoUnit.class)38void shouldWorkWithAllTimeUnits(ChronoUnit unit) {39 assertNotNull(unit);40}
Nested tests:
1class CalculatorTest {2 @Nested3 class Addition {4 @Test void positiveNumbers() { assertEquals(5, calc.add(2, 3)); }5 @Test void negativeNumbers() { assertEquals(-5, calc.add(-2, -3)); }6 }78 @Nested9 class Division {10 @Test void byZero() {11 assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));12 }13 }14}
Extensions (lifecycle hooks):
1public class DatabaseExtension implements BeforeAllCallback, AfterAllCallback,2 BeforeEachCallback, AfterEachCallback {3 @Override4 public void beforeAll(ExtensionContext ctx) { startDatabase(); }5 @Override6 public void afterAll(ExtensionContext ctx) { stopDatabase(); }7 @Override8 public void beforeEach(ExtensionContext ctx) { truncateTables(); }9 @Override10 public void afterEach(ExtensionContext ctx) { logTestResult(ctx); }11}1213@ExtendWith(DatabaseExtension.class)14@Testclass15public class UserRepositoryTest { ... }
Tags (filtering):
1@Tag("slow")2@Test void longRunningTest() { }34# Run: mvn test -Dgroups="!slow"
Dynamic tests (runtime-generated):
1@TestFactory2Stream<DynamicTest> dynamicShoppingCartTests() {3 ShoppingCart cart = new ShoppingCart();4 return Stream.of(5 dynamicTest("Add item increases size", () -> {6 cart.add(new Item("Widget", 9.99));7 assertEquals(1, cart.size());8 }),9 dynamicTest("Remove item decreases size", () -> {10 cart.remove(0);11 assertEquals(0, cart.size());12 })13 );14}
Common mistakes:
@ExtendWith — custom extensions are silently ignored without it.org.junit.jupiter.api.Test is JUnit 5, org.junit.Test is JUnit 4. Mixing them in the same class leads to confusing behavior.@DisplayName — test output reads test1(), test2() instead of meaningful descriptions.assertEquals(expected, actual, "description") for clearer failure reports.