JUnit Jupiter extension for parameterized tests
—
Extension points for custom argument providers and the fundamental interfaces that power all argument sources.
Registers custom ArgumentsProvider implementations for specialized test data generation.
/**
* Registers a custom ArgumentsProvider implementation
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@Repeatable(ArgumentsSources.class)
@interface ArgumentsSource {
/**
* ArgumentsProvider implementation class
*/
Class<? extends ArgumentsProvider> value();
}Core contract for providing streams of Arguments to parameterized tests.
/**
* Contract for providing streams of Arguments to parameterized tests
* 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 ArgumentsProvider {
/**
* Provides a stream of Arguments for test invocations
*
* @param context the current extension context
* @return stream of Arguments, never null
* @throws Exception if argument generation fails
*/
Stream<? extends Arguments> provideArguments(ExtensionContext context)
throws Exception;
}Represents a set of arguments for one parameterized test invocation.
/**
* Represents a set of arguments for one parameterized test invocation
*/
@API(status = STABLE, since = "5.0")
interface Arguments {
/**
* Returns the arguments as an Object array
*/
Object[] get();
/**
* Factory method for creating Arguments from objects
*/
static Arguments of(Object... arguments) {
return () -> arguments;
}
/**
* Alias for of() method
*/
static Arguments arguments(Object... arguments) {
return of(arguments);
}
/**
* Creates a named argument set (experimental)
*/
@API(status = EXPERIMENTAL, since = "5.12")
static ArgumentSet argumentSet(String name, Object... arguments) {
return new ArgumentSet(name, arguments);
}
/**
* Named argument set with display name (experimental)
*/
@API(status = EXPERIMENTAL, since = "5.12")
class ArgumentSet implements Arguments {
private final String name;
private final Object[] arguments;
ArgumentSet(String name, Object[] arguments) {
this.name = name;
this.arguments = arguments;
}
/**
* Returns the argument set name
*/
public String getName() {
return name;
}
@Override
public Object[] get() {
return arguments;
}
}
}Usage Examples:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.lang.annotation.*;
import java.util.stream.Stream;
import java.time.*;
// Custom annotation with ArgumentsProvider
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(DateRangeProvider.class)
@interface DateRange {
String start();
String end();
int stepDays() default 1;
}
// Custom ArgumentsProvider implementation
class DateRangeProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
DateRange dateRange = context.getRequiredTestMethod()
.getAnnotation(DateRange.class);
LocalDate start = LocalDate.parse(dateRange.start());
LocalDate end = LocalDate.parse(dateRange.end());
int stepDays = dateRange.stepDays();
return start.datesUntil(end.plusDays(1), Period.ofDays(stepDays))
.map(Arguments::of);
}
}
class CustomSourceExamples {
// Using custom date range provider
@ParameterizedTest
@DateRange(start = "2023-01-01", end = "2023-01-05")
void testDateRange(LocalDate date) {
assertNotNull(date);
assertTrue(date.getYear() == 2023);
assertTrue(date.getMonthValue() == 1);
}
// Using custom provider with step
@ParameterizedTest
@DateRange(start = "2023-01-01", end = "2023-01-10", stepDays = 3)
void testDateRangeWithStep(LocalDate date) {
assertNotNull(date);
// Tests: 2023-01-01, 2023-01-04, 2023-01-07, 2023-01-10
}
// Direct ArgumentsSource usage
@ParameterizedTest
@ArgumentsSource(RandomNumberProvider.class)
void testWithRandomNumbers(int number) {
assertTrue(number >= 1 && number <= 100);
}
}
// Another custom provider example
class RandomNumberProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return new Random(42) // Fixed seed for reproducible tests
.ints(5, 1, 101) // 5 random integers between 1-100
.mapToObj(Arguments::of);
}
}Database-driven ArgumentsProvider:
import javax.sql.DataSource;
import java.sql.*;
// Custom annotation for database queries
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(DatabaseProvider.class)
@interface DatabaseQuery {
String sql();
String dataSource() default "default";
}
class DatabaseProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
DatabaseQuery query = context.getRequiredTestMethod()
.getAnnotation(DatabaseQuery.class);
try {
DataSource dataSource = getDataSource(query.dataSource());
return executeQuery(dataSource, query.sql());
} catch (SQLException e) {
throw new RuntimeException("Failed to execute database query", e);
}
}
private Stream<Arguments> executeQuery(DataSource dataSource, String sql)
throws SQLException {
// Implementation to execute SQL and return Arguments stream
// This is a simplified example
return Stream.empty();
}
private DataSource getDataSource(String name) {
// Implementation to get DataSource by name
return null;
}
}
class DatabaseTestExample {
@ParameterizedTest
@DatabaseQuery(sql = "SELECT id, name, price FROM products WHERE active = true")
void testActiveProducts(int id, String name, double price) {
assertTrue(id > 0);
assertNotNull(name);
assertTrue(price >= 0);
}
}Configuration-driven ArgumentsProvider:
import java.util.Properties;
import java.io.InputStream;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(ConfigurationProvider.class)
@interface ConfigurationSource {
String file();
String prefix() default "";
}
class ConfigurationProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
ConfigurationSource config = context.getRequiredTestMethod()
.getAnnotation(ConfigurationSource.class);
try {
Properties props = loadProperties(config.file());
String prefix = config.prefix();
return props.entrySet().stream()
.filter(entry -> entry.getKey().toString().startsWith(prefix))
.map(entry -> Arguments.of(
entry.getKey().toString(),
entry.getValue().toString()
));
} catch (Exception e) {
throw new RuntimeException("Failed to load configuration", e);
}
}
private Properties loadProperties(String filename) throws Exception {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream(filename)) {
props.load(input);
}
return props;
}
}
class ConfigurationTestExample {
@ParameterizedTest
@ConfigurationSource(file = "/test.properties", prefix = "api.")
void testApiConfiguration(String key, String value) {
assertTrue(key.startsWith("api."));
assertNotNull(value);
assertFalse(value.trim().isEmpty());
}
}Custom providers can consume configuration from annotations using the AnnotationConsumer interface.
/**
* Functional interface for consuming configuration annotations
*/
@API(status = STABLE, since = "5.0")
@FunctionalInterface
interface AnnotationConsumer<A extends Annotation> extends Consumer<A> {
// Inherits accept(A annotation) method from Consumer
}Example with AnnotationConsumer:
import org.junit.jupiter.params.support.AnnotationConsumer;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(ConfigurableRangeProvider.class)
@interface NumberRange {
int min() default 1;
int max() default 10;
int count() default 5;
}
class ConfigurableRangeProvider implements ArgumentsProvider,
AnnotationConsumer<NumberRange> {
private NumberRange range;
@Override
public void accept(NumberRange numberRange) {
this.range = numberRange;
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
Random random = new Random(42);
return random.ints(range.count(), range.min(), range.max() + 1)
.mapToObj(Arguments::of);
}
}
class AnnotationConsumerExample {
@ParameterizedTest
@NumberRange(min = 10, max = 20, count = 3)
void testWithConfigurableRange(int number) {
assertTrue(number >= 10 && number <= 20);
}
}Container annotation for multiple @ArgumentsSource annotations.
/**
* Container annotation for multiple @ArgumentsSource annotations
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@interface ArgumentsSources {
ArgumentsSource[] value();
}Usage Example:
class MultipleArgumentsSourceExample {
@ParameterizedTest
@ArgumentsSource(RandomNumberProvider.class)
@ArgumentsSource(SequentialNumberProvider.class)
void testWithMultipleSources(int number) {
assertTrue(number > 0);
// Tests with both random and sequential numbers
}
}
class SequentialNumberProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(1, 2, 3, 4, 5).map(Arguments::of);
}
}Custom sources provide unlimited flexibility for test data generation, enabling integration with external systems, dynamic data generation, and complex test scenarios that go beyond the built-in argument sources.
Install with Tessl CLI
npx tessl i tessl/maven-org-junit-jupiter--junit-jupiter-params