JUnit Jupiter extension for parameterized tests
—
Type conversion system with built-in converters and extension points for custom conversions between argument types.
Specifies explicit ArgumentConverter for parameter conversion.
/**
* Specifies an explicit ArgumentConverter for parameter conversion
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@interface ConvertWith {
/**
* ArgumentConverter implementation class
*/
Class<? extends ArgumentConverter> value();
}Core contract for converting arguments to target parameter types.
/**
* Contract for converting arguments to target types
* Implementations must be thread-safe and have a no-args constructor
* or a single constructor whose parameters can be resolved by JUnit
*/
@API(status = STABLE, since = "5.0")
@FunctionalInterface
interface ArgumentConverter {
/**
* Converts the supplied source object to target type
*
* @param source the source object to convert
* @param context parameter context providing target type information
* @return converted object, may be null
* @throws ArgumentConversionException if conversion fails
*/
Object convert(Object source, ParameterContext context)
throws ArgumentConversionException;
}Abstract base class for converters that only need the target type.
/**
* Abstract base class for converters that only need target type information
*/
@API(status = STABLE, since = "5.0")
abstract class SimpleArgumentConverter implements ArgumentConverter {
@Override
public final Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
return convert(source, context.getParameter().getType());
}
/**
* Converts source to target type
*
* @param source source object to convert, may be null
* @param targetType target type for conversion
* @return converted object, may be null
* @throws ArgumentConversionException if conversion fails
*/
protected abstract Object convert(Object source, Class<?> targetType)
throws ArgumentConversionException;
}Abstract base class for type-safe source-to-target conversion.
/**
* Abstract base class for type-safe source-to-target conversion
*/
@API(status = STABLE, since = "5.0")
abstract class TypedArgumentConverter<S, T> implements ArgumentConverter {
private final Class<S> sourceType;
private final Class<T> targetType;
/**
* Creates converter for specific source and target types
*/
protected TypedArgumentConverter(Class<S> sourceType, Class<T> targetType) {
this.sourceType = sourceType;
this.targetType = targetType;
}
@Override
public final Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
if (source == null) {
return null;
}
if (!sourceType.isInstance(source)) {
throw new ArgumentConversionException(
"Cannot convert " + source.getClass() + " to " + targetType);
}
return convert(sourceType.cast(source));
}
/**
* Converts source to target type with type safety
*
* @param source source object of type S, never null
* @return converted object of type T, may be null
* @throws ArgumentConversionException if conversion fails
*/
protected abstract T convert(S source) throws ArgumentConversionException;
}Usage Examples:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.converter.SimpleArgumentConverter;
import org.junit.jupiter.params.converter.TypedArgumentConverter;
import org.junit.jupiter.params.provider.ValueSource;
import java.time.LocalDate;
import java.util.Locale;
// Simple converter example
class StringToUpperCaseConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
if (source instanceof String && targetType == String.class) {
return ((String) source).toUpperCase();
}
throw new ArgumentConversionException("Cannot convert to " + targetType);
}
}
// Typed converter example
class StringToLocalDateConverter extends TypedArgumentConverter<String, LocalDate> {
StringToLocalDateConverter() {
super(String.class, LocalDate.class);
}
@Override
protected LocalDate convert(String source) {
return LocalDate.parse(source);
}
}
// Advanced converter with configuration
class StringToLocaleConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context) {
if (source instanceof String) {
String[] parts = ((String) source).split("_");
if (parts.length == 1) {
return new Locale(parts[0]);
} else if (parts.length == 2) {
return new Locale(parts[0], parts[1]);
}
}
throw new ArgumentConversionException("Cannot convert to Locale: " + source);
}
}
class ArgumentConverterExamples {
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "junit"})
void testWithUpperCase(@ConvertWith(StringToUpperCaseConverter.class) String value) {
assertEquals(value, value.toUpperCase());
// Tests: "HELLO", "WORLD", "JUNIT"
}
@ParameterizedTest
@ValueSource(strings = {"2023-01-01", "2023-12-31", "2024-02-29"})
void testWithDates(@ConvertWith(StringToLocalDateConverter.class) LocalDate date) {
assertNotNull(date);
assertTrue(date.getYear() >= 2023);
}
@ParameterizedTest
@ValueSource(strings = {"en", "en_US", "fr_FR"})
void testWithLocales(@ConvertWith(StringToLocaleConverter.class) Locale locale) {
assertNotNull(locale);
assertNotNull(locale.getLanguage());
}
}Built-in support for Java Time types with pattern-based conversion.
/**
* Converts strings to Java Time types using specified pattern
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@ConvertWith(JavaTimeArgumentConverter.class)
@interface JavaTimeConversionPattern {
/**
* Date/time pattern string
*/
String value();
/**
* Whether to allow null values (experimental)
*/
@API(status = EXPERIMENTAL, since = "5.12")
boolean nullable() default false;
}Java Time Conversion Examples:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.JavaTimeConversionPattern;
import org.junit.jupiter.params.provider.ValueSource;
import java.time.*;
class JavaTimeConversionExamples {
@ParameterizedTest
@ValueSource(strings = {"2023-01-01", "2023-12-31"})
void testWithLocalDate(LocalDate date) {
// Automatic conversion from ISO date strings
assertNotNull(date);
assertEquals(2023, date.getYear());
}
@ParameterizedTest
@ValueSource(strings = {"01/15/2023", "12/31/2023"})
void testWithCustomDatePattern(
@JavaTimeConversionPattern("MM/dd/yyyy") LocalDate date) {
assertNotNull(date);
assertEquals(2023, date.getYear());
}
@ParameterizedTest
@ValueSource(strings = {"14:30:00", "09:15:30"})
void testWithLocalTime(LocalTime time) {
// Automatic conversion from ISO time strings
assertNotNull(time);
}
@ParameterizedTest
@ValueSource(strings = {"2:30 PM", "9:15 AM"})
void testWithCustomTimePattern(
@JavaTimeConversionPattern("h:mm a") LocalTime time) {
assertNotNull(time);
}
@ParameterizedTest
@ValueSource(strings = {"2023-01-01T14:30:00", "2023-12-31T23:59:59"})
void testWithLocalDateTime(LocalDateTime dateTime) {
// Automatic conversion from ISO datetime strings
assertNotNull(dateTime);
assertEquals(2023, dateTime.getYear());
}
@ParameterizedTest
@ValueSource(strings = {"Jan 15, 2023 2:30:00 PM", "Dec 31, 2023 11:59:59 PM"})
void testWithCustomDateTimePattern(
@JavaTimeConversionPattern("MMM dd, yyyy h:mm:ss a") LocalDateTime dateTime) {
assertNotNull(dateTime);
assertEquals(2023, dateTime.getYear());
}
}Built-in converter that handles common implicit conversions.
/**
* Default converter providing implicit conversions for common types
*/
@API(status = STABLE, since = "5.0")
class DefaultArgumentConverter {
/**
* Converts source to target type using implicit conversion rules
*/
public static Object convert(Object source, Class<?> targetType)
throws ArgumentConversionException {
// Implementation handles:
// - String to primitive types and wrappers
// - String to enums
// - String to java.time types
// - String to java.io.File, java.nio.file.Path
// - String to java.net.URI, java.net.URL
// - String to Currency, Locale, UUID
// - Number conversions
// - And more...
}
}Interface for converters that consume configuration annotations.
/**
* Base interface for annotation-driven argument converters
*/
@API(status = STABLE, since = "5.0")
interface AnnotationBasedArgumentConverter<A extends Annotation, S, T>
extends ArgumentConverter, AnnotationConsumer<A> {
/**
* Converts source to target type using annotation configuration
*/
T convert(S source, Class<T> targetType) throws ArgumentConversionException;
}Exception thrown when argument conversion fails.
/**
* Exception thrown when argument conversion fails
*/
@API(status = STABLE, since = "5.0")
class ArgumentConversionException extends JUnitException {
/**
* Constructs exception with message
*/
ArgumentConversionException(String message) {
super(message);
}
/**
* Constructs exception with message and cause
*/
ArgumentConversionException(String message, Throwable cause) {
super(message, cause);
}
}Custom conversion with validation:
class ValidatedEmailConverter extends TypedArgumentConverter<String, Email> {
ValidatedEmailConverter() {
super(String.class, Email.class);
}
@Override
protected Email convert(String source) throws ArgumentConversionException {
if (!source.contains("@")) {
throw new ArgumentConversionException("Invalid email format: " + source);
}
return new Email(source);
}
}
class Email {
private final String address;
Email(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
}
class ValidationExample {
@ParameterizedTest
@ValueSource(strings = {"user@example.com", "test@domain.org"})
void testValidEmails(@ConvertWith(ValidatedEmailConverter.class) Email email) {
assertNotNull(email);
assertTrue(email.getAddress().contains("@"));
}
}Complex object conversion:
class JsonToObjectConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
if (source instanceof String) {
Class<?> targetType = context.getParameter().getType();
return parseJson((String) source, targetType);
}
throw new ArgumentConversionException("Cannot convert " + source.getClass());
}
private Object parseJson(String json, Class<?> targetType) {
// JSON parsing implementation
// This is a simplified example
return null;
}
}
class JsonConversionExample {
@ParameterizedTest
@ValueSource(strings = {
"{\"name\":\"Alice\",\"age\":25}",
"{\"name\":\"Bob\",\"age\":30}"
})
void testJsonConversion(@ConvertWith(JsonToObjectConverter.class) Person person) {
assertNotNull(person);
assertNotNull(person.getName());
assertTrue(person.getAge() > 0);
}
}The argument conversion system provides flexible type transformation capabilities, enabling seamless integration between various data sources and test parameter types while maintaining type safety and clear error reporting.
Install with Tessl CLI
npx tessl i tessl/maven-org-junit-jupiter--junit-jupiter-params