CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-junit-jupiter--junit-jupiter-params

JUnit Jupiter extension for parameterized tests

Pending
Overview
Eval results
Files

custom-sources.mddocs/

Custom Sources

Extension points for custom argument providers and the fundamental interfaces that power all argument sources.

Capabilities

@ArgumentsSource Annotation

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();
}

ArgumentsProvider Interface

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;
}

Arguments Interface

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);
    }
}

Advanced Custom Provider Patterns

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());
    }
}

AnnotationConsumer Support

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

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

docs

argument-aggregation.md

argument-conversion.md

core-testing.md

csv-sources.md

custom-sources.md

enum-method-sources.md

index.md

value-sources.md

tile.json