Parameterized Unit Tests with JUnit 5
Overview
This skill provides patterns for writing efficient parameterized unit tests using JUnit 5's @ParameterizedTest. It covers @ValueSource, @CsvSource, @MethodSource, @EnumSource, @ArgumentsSource, and custom display names to run the same test logic with multiple input values, reducing test duplication and improving coverage.
When to Use
Use this skill when:
-
Testing methods with multiple valid inputs
-
Testing boundary values systematically
-
Testing multiple invalid inputs for error cases
-
Want to reduce test duplication
-
Testing multiple scenarios with similar assertions
-
Need data-driven testing approach
Instructions
-
Add junit-jupiter-params dependency: Ensure junit-jupiter-params is on test classpath
-
Choose appropriate source: Use @ValueSource for simple values, @CsvSource for tabular data, @MethodSource for complex objects
-
Match parameter types: Ensure test method parameters match data source types
-
Use descriptive display names: Set name = "..." for readable test output
-
Test boundary values: Include edge cases, null values, and extreme values in parameters
-
Use @EnumSource: Test all enum values or filter specific ones
-
Create custom ArgumentsProvider: Build reusable data sources for complex scenarios
-
Keep assertions simple: Focus on single assertion per parameterized test
Examples
Setup: Parameterized Testing
Maven
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency>
Gradle
dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.assertj:assertj-core") }
Basic Pattern: @ValueSource
Simple Value Testing
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.*;
class StringUtilsTest {
@ParameterizedTest @ValueSource(strings = {"hello", "world", "test"}) void shouldCapitalizeAllStrings(String input) { String result = StringUtils.capitalize(input); assertThat(result).startsWith(input.substring(0, 1).toUpperCase()); }
@ParameterizedTest @ValueSource(ints = {1, 2, 3, 4, 5}) void shouldBePositive(int number) { assertThat(number).isPositive(); }
@ParameterizedTest @ValueSource(booleans = {true, false}) void shouldHandleBothBooleanValues(boolean value) { assertThat(value).isNotNull(); } }
@MethodSource for Complex Data
Factory Method Data Source
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream;
class CalculatorTest {
static Stream<org.junit.jupiter.params.provider.Arguments> additionTestCases() { return Stream.of( Arguments.of(1, 2, 3), Arguments.of(0, 0, 0), Arguments.of(-1, 1, 0), Arguments.of(100, 200, 300), Arguments.of(-5, -10, -15) ); }
@ParameterizedTest @MethodSource("additionTestCases") void shouldAddNumbersCorrectly(int a, int b, int expected) { int result = Calculator.add(a, b); assertThat(result).isEqualTo(expected); } }
@CsvSource for Tabular Data
CSV-Based Test Data
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource;
class UserValidationTest {
@ParameterizedTest @CsvSource({ "alice@example.com, true", "bob@gmail.com, true", "invalid-email, false", "user@, false", "@example.com, false", "user name@example.com, false" }) void shouldValidateEmailAddresses(String email, boolean expected) { boolean result = UserValidator.isValidEmail(email); assertThat(result).isEqualTo(expected); }
@ParameterizedTest @CsvSource({ "123-456-7890, true", "555-123-4567, true", "1234567890, false", "123-45-6789, false", "abc-def-ghij, false" }) void shouldValidatePhoneNumbers(String phone, boolean expected) { boolean result = PhoneValidator.isValid(phone); assertThat(result).isEqualTo(expected); } }
@CsvFileSource for External Data
CSV File-Based Testing
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource;
class PriceCalculationTest {
@ParameterizedTest @CsvFileSource(resources = "/test-data/prices.csv", numLinesToSkip = 1) void shouldCalculateTotalPrice(String product, double price, int quantity, double expected) { double total = PriceCalculator.calculateTotal(price, quantity); assertThat(total).isEqualTo(expected); } }
// test-data/prices.csv: // product,price,quantity,expected // Laptop,999.99,1,999.99 // Mouse,29.99,3,89.97 // Keyboard,79.99,2,159.98
@EnumSource for Enum Testing
Enum-Based Test Data
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource;
enum Status { ACTIVE, INACTIVE, PENDING, DELETED }
class StatusHandlerTest {
@ParameterizedTest @EnumSource(Status.class) void shouldHandleAllStatuses(Status status) { assertThat(status).isNotNull(); }
@ParameterizedTest @EnumSource(value = Status.class, names = {"ACTIVE", "INACTIVE"}) void shouldHandleSpecificStatuses(Status status) { assertThat(status).isIn(Status.ACTIVE, Status.INACTIVE); }
@ParameterizedTest @EnumSource(value = Status.class, mode = EnumSource.Mode.EXCLUDE, names = {"DELETED"}) void shouldHandleStatusesExcludingDeleted(Status status) { assertThat(status).isNotEqualTo(Status.DELETED); } }
Custom Display Names
Readable Test Output
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource;
class DiscountCalculationTest {
@ParameterizedTest(name = "Discount of {0}% should be calculated correctly") @ValueSource(ints = {5, 10, 15, 20}) void shouldApplyDiscount(int discountPercent) { double originalPrice = 100.0; double discounted = DiscountCalculator.apply(originalPrice, discountPercent); double expected = originalPrice * (1 - discountPercent / 100.0);
assertThat(discounted).isEqualTo(expected);
}
@ParameterizedTest(name = "User role {0} should have {1} permissions") @CsvSource({ "ADMIN, 100", "MANAGER, 50", "USER, 10" }) void shouldHaveCorrectPermissions(String role, int expectedPermissions) { User user = new User(role); assertThat(user.getPermissionCount()).isEqualTo(expectedPermissions); } }
Combining Multiple Sources
ArgumentsProvider for Complex Scenarios
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import java.util.stream.Stream;
class RangeValidatorArgumentProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(0, 0, 100, true), // Min boundary Arguments.of(100, 0, 100, true), // Max boundary Arguments.of(50, 0, 100, true), // Middle value Arguments.of(-1, 0, 100, false), // Below range Arguments.of(101, 0, 100, false) // Above range ); } }
class RangeValidatorTest {
@ParameterizedTest @ArgumentsSource(RangeValidatorArgumentProvider.class) void shouldValidateRangeCorrectly(int value, int min, int max, boolean expected) { boolean result = RangeValidator.isInRange(value, min, max); assertThat(result).isEqualTo(expected); } }
Testing Edge Cases with Parameters
Boundary Value Analysis
class BoundaryValueTest {
@ParameterizedTest @ValueSource(ints = { Integer.MIN_VALUE, // Absolute minimum Integer.MIN_VALUE + 1, // Just above minimum -1, // Negative boundary 0, // Zero boundary 1, // Just above zero Integer.MAX_VALUE - 1, // Just below maximum Integer.MAX_VALUE // Absolute maximum }) void shouldHandleAllBoundaryValues(int value) { int incremented = MathUtils.increment(value); assertThat(incremented).isNotLessThan(value); }
@ParameterizedTest @CsvSource({ ", false", // null "'', false", // empty "' ', false", // whitespace only "a, true", // single character "abc, true" // normal }) void shouldValidateStrings(String input, boolean expected) { boolean result = StringValidator.isValid(input); assertThat(result).isEqualTo(expected); } }
Repeat Tests
Run Same Test Multiple Times
import org.junit.jupiter.api.RepeatedTest;
class ConcurrencyTest {
@RepeatedTest(100) void shouldHandleConcurrentAccess() { // Test that might reveal race conditions if run multiple times AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); assertThat(counter.get()).isEqualTo(1); } }
Best Practices
-
Use @ParameterizedTest to reduce test duplication
-
Use descriptive display names with (name = "...")
-
Test boundary values systematically
-
Keep test logic simple - focus on single assertion
-
Organize test data logically - group similar scenarios
-
Use @MethodSource for complex test data
-
Use @CsvSource for tabular test data
-
Document expected behavior in test names
Common Patterns
Testing error conditions:
@ParameterizedTest @ValueSource(strings = {"", " ", null}) void shouldThrowExceptionForInvalidInput(String input) { assertThatThrownBy(() -> Parser.parse(input)) .isInstanceOf(IllegalArgumentException.class); }
Testing multiple valid inputs:
@ParameterizedTest @ValueSource(ints = {1, 2, 3, 5, 8, 13}) void shouldBeInFibonacciSequence(int number) { assertThat(FibonacciChecker.isFibonacci(number)).isTrue(); }
Constraints and Warnings
-
Parameter count must match: The number of parameters from source must match test method signature
-
Type conversion is automatic: JUnit converts source values to target parameter types when possible
-
@ValueSource limitation: Only supports literals (strings, ints, longs, doubles); not objects or null
-
CSV escaping: Strings containing commas must be enclosed in single quotes in @CsvSource
-
MethodSource visibility: @MethodSource methods must be static, can be private but must be in same class
-
Display name placeholders: Use {0}, {1}, etc. to reference parameters in display names
-
Test execution order: Parameterized tests execute each parameter set as a separate test invocation
Troubleshooting
Parameter not matching: Verify number and type of parameters match test method signature.
Display name not showing: Check parameter syntax in name = "..." .
CSV parsing error: Ensure CSV format is correct and quote strings containing commas.
References
-
JUnit 5 Parameterized Tests
-
@ParameterizedTest Documentation
-
Boundary Value Analysis