@DisplayName and @ParameterizedTest in JUnit 5

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper