JUnit 5 introduces powerful testing features including @DisplayName for readable test names and @ParameterizedTest for data-driven testing. This comprehensive guide covers both annotations with practical examples and best practices.
@DisplayName Annotation
1. Basic @DisplayName Usage
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @DisplayName("Calculator Operations Test Suite") class CalculatorTest { private final Calculator calculator = new Calculator(); @Test @DisplayName("Addition of two positive numbers") void testAdditionPositiveNumbers() { assertEquals(5, calculator.add(2, 3)); } @Test @DisplayName("Addition with zero") void testAdditionWithZero() { assertEquals(3, calculator.add(3, 0)); assertEquals(3, calculator.add(0, 3)); } @Test @DisplayName("❌ Division by zero should throw exception") void testDivisionByZero() { assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0)); } @Test @DisplayName("✅ Multiplication of large numbers") void testMultiplicationLargeNumbers() { assertEquals(1_000_000, calculator.multiply(1000, 1000)); } } 2. @DisplayName with Nested Tests
@DisplayName("User Service Test Suite") class UserServiceTest { private UserService userService = new UserService(); @Nested @DisplayName("User Creation Tests") class UserCreationTests { @Test @DisplayName("Create user with valid data") void createUser_withValidData_shouldSucceed() { User user = userService.createUser("john.doe", "[email protected]"); assertNotNull(user); assertEquals("john.doe", user.getUsername()); } @Test @DisplayName("Create user with duplicate username should fail") void createUser_withDuplicateUsername_shouldThrowException() { userService.createUser("jane.doe", "[email protected]"); assertThrows(DuplicateUserException.class, () -> userService.createUser("jane.doe", "[email protected]")); } } @Nested @DisplayName("User Validation Tests") class UserValidationTests { @Test @DisplayName("Validate user with correct credentials") void validateUser_withCorrectCredentials_shouldReturnTrue() { User user = userService.createUser("testuser", "[email protected]"); boolean isValid = userService.validateUser("testuser", "correctPassword"); assertTrue(isValid); } @Test @DisplayName("Validate user with incorrect password") void validateUser_withIncorrectPassword_shouldReturnFalse() { User user = userService.createUser("testuser", "[email protected]"); boolean isValid = userService.validateUser("testuser", "wrongPassword"); assertFalse(isValid); } } } @ParameterizedTest Basics
3. Simple Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.*; class StringUtilsTest { @ParameterizedTest @ValueSource(strings = {"racecar", "radar", "level", "madam"}) @DisplayName("Palindrome strings should return true") void testIsPalindrome_withPalindromes_shouldReturnTrue(String input) { assertTrue(StringUtils.isPalindrome(input)); } @ParameterizedTest @ValueSource(ints = {2, 4, 6, 8, 10}) @DisplayName("Even numbers should be identified as even") void testIsEven_withEvenNumbers_shouldReturnTrue(int number) { assertTrue(NumberUtils.isEven(number)); } @ParameterizedTest @ValueSource(booleans = {true, false}) @DisplayName("Boolean negation test") void testBooleanNegation(boolean input) { assertEquals(!input, BooleanUtils.negate(input)); } } 4. @CsvSource for Multiple Parameters
class MathOperationsTest { @ParameterizedTest @CsvSource({ "2, 3, 5", // a, b, expectedSum "0, 5, 5", // zero addition "-2, -3, -5", // negative numbers "10, -5, 5" // positive and negative }) @DisplayName("Addition of two numbers") void testAddition(int a, int b, int expectedSum) { assertEquals(expectedSum, MathOperations.add(a, b)); } @ParameterizedTest @CsvSource({ "10, 2, 5.0", // normal division "0, 5, 0.0", // zero numerator "-10, 2, -5.0", // negative numerator "10, -2, -5.0" // negative denominator }) @DisplayName("Division of two numbers") void testDivision(int numerator, int denominator, double expectedResult) { assertEquals(expectedResult, MathOperations.divide(numerator, denominator), 0.001); } @ParameterizedTest @CsvSource(delimiter = '|', value = { "apple | 5 | appleappleappleappleapple", "test | 3 | testtesttest", "x | 1 | x", "'' | 5 | ''" }) @DisplayName("String repetition with custom delimiter") void testStringRepeat(String input, int count, String expected) { assertEquals(expected, StringUtils.repeat(input, count)); } } Advanced Parameterized Test Sources
5. @MethodSource for Complex Data
class AdvancedParameterizedTest { static Stream<Arguments> userAgeProvider() { return Stream.of( Arguments.of("John", 25, true), // Adult Arguments.of("Jane", 17, false), // Minor Arguments.of("Bob", 18, true), // Exactly 18 Arguments.of("Alice", 65, true) // Senior ); } @ParameterizedTest @MethodSource("userAgeProvider") @DisplayName("Check if user is adult based on age") void testIsAdult(String name, int age, boolean expectedIsAdult) { User user = new User(name, age); assertEquals(expectedIsAdult, user.isAdult()); } static Stream<Arguments> triangleProvider() { return Stream.of( Arguments.of(3, 4, 5, 6.0), // Right triangle Arguments.of(5, 5, 5, 10.825), // Equilateral triangle Arguments.of(6, 8, 10, 24.0), // Right triangle Arguments.of(7, 8, 9, 26.833) // Scalene triangle ); } @ParameterizedTest @MethodSource("triangleProvider") @DisplayName("Calculate triangle area using Heron's formula") void testTriangleArea(double a, double b, double c, double expectedArea) { double actualArea = GeometryUtils.triangleArea(a, b, c); assertEquals(expectedArea, actualArea, 0.001); } // Factory method with complex objects static Stream<Arguments> productProvider() { return Stream.of( Arguments.of(new Product("Laptop", 999.99, "ELECTRONICS"), 1099.99), Arguments.of(new Product("Book", 19.99, "EDUCATION"), 19.99), Arguments.of(new Product("Chair", 149.99, "FURNITURE"), 164.99) ); } @ParameterizedTest @MethodSource("productProvider") @DisplayName("Calculate product price with tax") void testProductPriceWithTax(Product product, double expectedPriceWithTax) { double actualPrice = product.getPriceWithTax(); assertEquals(expectedPriceWithTax, actualPrice, 0.01); } } 6. @EnumSource for Enum Testing
enum DayOfWeek { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } class DayOfWeekTest { @ParameterizedTest @EnumSource(DayOfWeek.class) @DisplayName("All days should have a valid name") void testDayNames(DayOfWeek day) { assertNotNull(day.name()); assertFalse(day.name().isEmpty()); } @ParameterizedTest @EnumSource(value = DayOfWeek.class, names = {"MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"}) @DisplayName("Weekdays should be working days") void testWeekdaysAreWorkingDays(DayOfWeek day) { assertTrue(day.isWorkingDay()); } @ParameterizedTest @EnumSource(value = DayOfWeek.class, mode = EnumSource.Mode.EXCLUDE, names = {"SATURDAY", "SUNDAY"}) @DisplayName("Days excluding weekends are weekdays") void testDaysExcludingWeekends(DayOfWeek day) { assertTrue(day.isWorkingDay()); } @ParameterizedTest @EnumSource(value = DayOfWeek.class, mode = EnumSource.Mode.MATCH_ALL, names = ".*DAY") @DisplayName("All days should end with 'DAY'") void testAllDaysEndWithDay(DayOfWeek day) { assertTrue(day.name().endsWith("DAY")); } } Combined @DisplayName and @ParameterizedTest
7. Comprehensive Example
@DisplayName("Bank Account Management Test Suite") class BankAccountTest { @Nested @DisplayName("Deposit Operations") class DepositTests { @ParameterizedTest @CsvSource({ "100.00, 50.00, 150.00", "0.00, 100.00, 100.00", "500.00, 0.01, 500.01" }) @DisplayName("✅ Successful deposits should increase balance") void deposit_positiveAmount_shouldIncreaseBalance( double initialBalance, double depositAmount, double expectedBalance) { BankAccount account = new BankAccount("123", initialBalance); account.deposit(depositAmount); assertEquals(expectedBalance, account.getBalance(), 0.001); } @ParameterizedTest @ValueSource(doubles = {-100.00, -0.01, -1.00}) @DisplayName("❌ Deposit with negative amount should throw exception") void deposit_negativeAmount_shouldThrowException(double invalidAmount) { BankAccount account = new BankAccount("123", 100.00); assertThrows(IllegalArgumentException.class, () -> account.deposit(invalidAmount)); } } @Nested @DisplayName("Withdrawal Operations") class WithdrawalTests { @ParameterizedTest @MethodSource("validWithdrawalProvider") @DisplayName("✅ Successful withdrawals should decrease balance") void withdraw_validAmount_shouldDecreaseBalance( double initialBalance, double withdrawAmount, double expectedBalance) { BankAccount account = new BankAccount("123", initialBalance); account.withdraw(withdrawAmount); assertEquals(expectedBalance, account.getBalance(), 0.001); } static Stream<Arguments> validWithdrawalProvider() { return Stream.of( Arguments.of(100.00, 50.00, 50.00), Arguments.of(200.00, 200.00, 0.00), Arguments.of(500.00, 0.01, 499.99) ); } @ParameterizedTest @CsvSource({ "50.00, 100.00", "0.00, 0.01", "10.00, 10.01" }) @DisplayName("❌ Withdrawal exceeding balance should throw exception") void withdraw_amountExceedingBalance_shouldThrowException( double initialBalance, double withdrawAmount) { BankAccount account = new BankAccount("123", initialBalance); assertThrows(InsufficientFundsException.class, () -> account.withdraw(withdrawAmount)); } } } Custom Argument Sources
8. Custom Provider with @ArgumentsSource
// Custom argument provider for file testing class FileArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of( Arguments.of("test.txt", "text/plain", true), Arguments.of("document.pdf", "application/pdf", true), Arguments.of("image.jpg", "image/jpeg", true), Arguments.of("script.exe", "application/octet-stream", false), Arguments.of("data.json", "application/json", true) ); } } class FileProcessorTest { @ParameterizedTest @ArgumentsSource(FileArgumentsProvider.class) @DisplayName("Validate supported file types") void testSupportedFileTypes(String filename, String mimeType, boolean expectedSupported) { FileProcessor processor = new FileProcessor(); assertEquals(expectedSupported, processor.isSupportedFileType(filename, mimeType)); } } // Custom provider for user roles class UserRoleArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(new User("admin", "[email protected]", Role.ADMIN), true, true, true), Arguments.of(new User("editor", "[email protected]", Role.EDITOR), false, true, true), Arguments.of(new User("viewer", "[email protected]", Role.VIEWER), false, false, true), Arguments.of(new User("guest", "[email protected]", Role.GUEST), false, false, false) ); } } class AuthorizationTest { @ParameterizedTest @ArgumentsSource(UserRoleArgumentsProvider.class) @DisplayName("Check user permissions based on role") void testUserPermissions(User user, boolean canDelete, boolean canEdit, boolean canView) { PermissionService service = new PermissionService(); assertEquals(canDelete, service.canDelete(user)); assertEquals(canEdit, service.canEdit(user)); assertEquals(canView, service.canView(user)); } } Dynamic Tests with Parameterized Tests
9. Combining @TestFactory with Parameterized Tests
class DynamicParameterizedTest { @TestFactory @DisplayName("Dynamic test generation for mathematical operations") Stream<DynamicTest> generateMathOperationTests() { List<Triple<String, Double, Double>> testCases = Arrays.asList( Triple.of("Square root of 4", 4.0, 2.0), Triple.of("Square root of 9", 9.0, 3.0), Triple.of("Square root of 16", 16.0, 4.0), Triple.of("Square root of 25", 25.0, 5.0) ); return testCases.stream() .map(testCase -> DynamicTest.dynamicTest( "Calculate " + testCase.getLeft(), () -> { double result = Math.sqrt(testCase.getMiddle()); assertEquals(testCase.getRight(), result, 0.001); } )); } @TestFactory @DisplayName("Dynamic tests for string manipulation") Stream<DynamicTest> generateStringManipulationTests() { return Stream.of( Arguments.of("hello", "HELLO"), Arguments.of("WORLD", "WORLD"), Arguments.of("MiXeD", "MIXED"), Arguments.of("", "") ) .map(arguments -> DynamicTest.dynamicTest( "Convert '" + arguments.get()[0] + "' to uppercase", () -> { String input = (String) arguments.get()[0]; String expected = (String) arguments.get()[1]; assertEquals(expected, input.toUpperCase()); } )); } } Best Practices and Configuration
10. Test Configuration and Utilities
// Custom annotation for common parameterized test configurations @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @ParameterizedTest @ValueSource(ints = {1, 2, 3, 5, 8, 13, 21}) @DisplayName("Fibonacci number test") public @interface FibonacciTest { } class MathUtilsTest { @FibonacciTest void testIsFibonacciNumber(int number) { assertTrue(MathUtils.isFibonacci(number)); } } // Test configuration class @TestInstance(TestInstance.Lifecycle.PER_CLASS) @DisplayName("Advanced Parameterized Test Configuration") class AdvancedTestConfiguration { private TestInfo testInfo; private TestReporter testReporter; @BeforeEach void init(TestInfo testInfo, TestReporter testReporter) { this.testInfo = testInfo; this.testReporter = testReporter; } @ParameterizedTest @CsvSource({ "2, true", "3, true", "4, false", "17, true", "25, false" }) @DisplayName("Prime number verification with logging") void testPrimeNumbers(int number, boolean expectedIsPrime) { testReporter.publishEntry( "Testing number", String.valueOf(number) ); assertEquals(expectedIsPrime, MathUtils.isPrime(number), () -> String.format("Number %d prime check failed", number)); // Log test information System.out.printf("Test: %s - Number: %d, Expected Prime: %b%n", testInfo.getDisplayName(), number, expectedIsPrime); } } // Utility class for test data generation class TestDataGenerator { static Stream<Arguments> generateUserTestData() { return IntStream.range(1, 6) .mapToObj(i -> Arguments.of( "user" + i, "user" + i + "@example.com", i * 10, i % 2 == 0 // Every even user is active )); } static Stream<Arguments> generateProductTestData() { return Stream.of( Arguments.of("Product A", 19.99, 10, true), Arguments.of("Product B", 29.99, 0, false), Arguments.of("Product C", 9.99, 5, true), Arguments.of("Product D", 49.99, 100, true) ); } } class DataDrivenTest { @ParameterizedTest @MethodSource("com.example.TestDataGenerator#generateUserTestData") @DisplayName("User creation with generated test data") void testUserCreationWithGeneratedData(String username, String email, int age, boolean active) { User user = new User(username, email, age); user.setActive(active); assertNotNull(user); assertEquals(username, user.getUsername()); assertEquals(email, user.getEmail()); assertEquals(age, user.getAge()); assertEquals(active, user.isActive()); } } Common Patterns and Tips
11. Best Practices Summary
class BestPracticesExample { // ✅ GOOD: Descriptive display names @ParameterizedTest @ValueSource(strings = {"admin", "user", "guest"}) @DisplayName("User with role {0} should have appropriate permissions") void testUserPermissions(String role) { // Test implementation } // ✅ GOOD: Use method source for complex data @ParameterizedTest @MethodSource("provideTestCases") @DisplayName("Complex business logic validation") void testBusinessLogic(Input input, ExpectedResult expected) { // Test implementation } // ✅ GOOD: Combine different argument sources @ParameterizedTest @CsvFileSource(resources = "/test-data.csv") @DisplayName("Load test data from CSV file") void testWithCsvFileSource(String input, int expected) { // Test implementation } // ❌ AVOID: Unclear test names @ParameterizedTest @ValueSource(ints = {1, 2, 3}) void test1(int number) { // Poor name // Test implementation } // ✅ GOOD: Use assertions with messages @ParameterizedTest @CsvSource({"5, 25", "3, 9", "4, 16"}) @DisplayName("Square calculation test") void testSquare(int input, int expected) { int result = input * input; assertEquals(expected, result, () -> String.format("Square of %d should be %d but was %d", input, expected, result)); } } Key Benefits Summary
- @DisplayName: Provides human-readable test names that appear in test reports and IDEs
- @ParameterizedTest: Enables data-driven testing with multiple input combinations
- Multiple Data Sources:
@ValueSource,@CsvSource,@MethodSource,@EnumSource,@ArgumentsSource - Combined Usage: Use both annotations for clear, data-driven tests
- Dynamic Tests: Generate tests programmatically for complex scenarios
- Custom Providers: Create reusable argument providers for complex test data
These features make JUnit 5 tests more expressive, maintainable, and comprehensive, especially when testing multiple input combinations or complex business logic.