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

argument-aggregation.mddocs/

Argument Aggregation

System for combining multiple arguments into single parameter objects with type-safe access and custom aggregation logic.

Capabilities

@AggregateWith Annotation

Specifies ArgumentsAggregator for combining multiple arguments into a single parameter.

/**
 * Specifies an ArgumentsAggregator for combining multiple arguments
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@interface AggregateWith {
    /**
     * ArgumentsAggregator implementation class
     */
    Class<? extends ArgumentsAggregator> value();
}

ArgumentsAccessor Interface

Type-safe access to argument arrays with automatic conversion support.

/**
 * Provides type-safe access to arguments with automatic conversion
 */
@API(status = STABLE, since = "5.0")
interface ArgumentsAccessor {
    
    /**
     * Gets argument at index as Object
     */
    Object get(int index);
    
    /**
     * Gets argument at index with type conversion
     */
    <T> T get(int index, Class<T> requiredType);
    
    /**
     * Gets argument as Character
     */
    Character getCharacter(int index);
    
    /**
     * Gets argument as Boolean
     */
    Boolean getBoolean(int index);
    
    /**
     * Gets argument as Byte
     */
    Byte getByte(int index);
    
    /**
     * Gets argument as Short
     */
    Short getShort(int index);
    
    /**
     * Gets argument as Integer
     */
    Integer getInteger(int index);
    
    /**
     * Gets argument as Long
     */
    Long getLong(int index);
    
    /**
     * Gets argument as Float
     */
    Float getFloat(int index);
    
    /**
     * Gets argument as Double
     */
    Double getDouble(int index);
    
    /**
     * Gets argument as String
     */
    String getString(int index);
    
    /**
     * Returns number of arguments
     */
    int size();
    
    /**
     * Converts all arguments to Object array
     */
    Object[] toArray();
    
    /**
     * Converts all arguments to List
     */
    List<Object> toList();
    
    /**
     * Gets current invocation index (0-based)
     */
    int getInvocationIndex();
}

ArgumentsAggregator Interface

Contract for aggregating multiple arguments into a single object.

/**
 * Contract for aggregating multiple arguments into a single object
 * 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 ArgumentsAggregator {
    
    /**
     * Aggregates multiple arguments into single object
     * 
     * @param accessor type-safe access to arguments
     * @param context parameter context for target type information
     * @return aggregated object, may be null
     * @throws ArgumentsAggregationException if aggregation fails
     */
    Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
        throws ArgumentsAggregationException;
}

Usage Examples:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.api.extension.ParameterContext;

// Person class for aggregation examples
class Person {
    private final String name;
    private final int age;
    private final boolean active;
    
    Person(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
    
    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
}

// Custom aggregator implementation
class PersonAggregator implements ArgumentsAggregator {
    
    @Override
    public Person aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) {
        return new Person(
            accessor.getString(0),
            accessor.getInteger(1), 
            accessor.getBoolean(2)
        );
    }
}

class ArgumentAggregationExamples {

    // Direct ArgumentsAccessor usage
    @ParameterizedTest
    @CsvSource({
        "Alice, 25, true",
        "Bob, 30, false", 
        "Charlie, 35, true"
    })
    void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
        String name = arguments.getString(0);
        int age = arguments.getInteger(1);
        boolean active = arguments.getBoolean(2);
        
        assertNotNull(name);
        assertTrue(age > 0);
        
        // Can also access by type conversion
        String ageString = arguments.get(1, String.class);
        assertEquals(String.valueOf(age), ageString);
    }

    // Custom aggregator usage
    @ParameterizedTest
    @CsvSource({
        "Alice, 25, true",
        "Bob, 30, false",
        "Charlie, 35, true" 
    })
    void testWithCustomAggregator(@AggregateWith(PersonAggregator.class) Person person) {
        assertNotNull(person);
        assertNotNull(person.getName());
        assertTrue(person.getAge() > 0);
    }

    // Mixed parameters with aggregation
    @ParameterizedTest
    @CsvSource({
        "1, Alice, 25, true",
        "2, Bob, 30, false"
    })
    void testMixedParameters(int id, @AggregateWith(PersonAggregator.class) Person person) {
        assertTrue(id > 0);
        assertNotNull(person);
        assertNotNull(person.getName());
    }
}

Advanced Aggregation Patterns

Generic aggregator for any class:

import java.lang.reflect.Constructor;

class ReflectiveAggregator implements ArgumentsAggregator {
    
    @Override
    public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) 
            throws ArgumentsAggregationException {
        try {
            Class<?> targetType = context.getParameter().getType();
            Constructor<?> constructor = findMatchingConstructor(targetType, accessor.size());
            
            Object[] args = new Object[accessor.size()];
            Class<?>[] paramTypes = constructor.getParameterTypes();
            
            for (int i = 0; i < accessor.size(); i++) {
                args[i] = accessor.get(i, paramTypes[i]);
            }
            
            return constructor.newInstance(args);
        } catch (Exception e) {
            throw new ArgumentsAggregationException("Aggregation failed", e);
        }
    }
    
    private Constructor<?> findMatchingConstructor(Class<?> clazz, int argCount) {
        for (Constructor<?> constructor : clazz.getConstructors()) {
            if (constructor.getParameterCount() == argCount) {
                return constructor;
            }
        }
        throw new RuntimeException("No matching constructor found");
    }
}

class Product {
    private final String name;
    private final double price;
    private final boolean available;
    
    public Product(String name, double price, boolean available) {
        this.name = name;
        this.price = price;
        this.available = available;
    }
    
    // Getters
    public String getName() { return name; }
    public double getPrice() { return price; }
    public boolean isAvailable() { return available; }
}

class ReflectiveAggregationExample {
    
    @ParameterizedTest
    @CsvSource({
        "Apple, 1.50, true",
        "Banana, 0.75, false"
    })
    void testReflectiveAggregation(@AggregateWith(ReflectiveAggregator.class) Product product) {
        assertNotNull(product);
        assertNotNull(product.getName());
        assertTrue(product.getPrice() >= 0);
    }
}

Builder pattern aggregator:

class UserBuilder {
    private String name;
    private int age;
    private String email;
    private boolean active = true;
    
    public UserBuilder name(String name) {
        this.name = name;
        return this;
    }
    
    public UserBuilder age(int age) {
        this.age = age;
        return this;
    }
    
    public UserBuilder email(String email) {
        this.email = email;
        return this;
    }
    
    public UserBuilder active(boolean active) {
        this.active = active;
        return this;
    }
    
    public User build() {
        return new User(name, age, email, active);
    }
}

class User {
    private final String name, email;
    private final int age;
    private final boolean active;
    
    User(String name, int age, String email, boolean active) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.active = active;
    }
    
    // Getters
    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
}

class UserBuilderAggregator implements ArgumentsAggregator {
    
    @Override
    public User aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) {
        return new UserBuilder()
            .name(accessor.getString(0))
            .age(accessor.getInteger(1))
            .email(accessor.getString(2))
            .active(accessor.getBoolean(3))
            .build();
    }
}

class BuilderAggregationExample {
    
    @ParameterizedTest
    @CsvSource({
        "Alice, 25, alice@example.com, true",
        "Bob, 30, bob@example.com, false"
    })
    void testBuilderAggregation(@AggregateWith(UserBuilderAggregator.class) User user) {
        assertNotNull(user);
        assertNotNull(user.getName());
        assertTrue(user.getEmail().contains("@"));
        assertTrue(user.getAge() > 0);
    }
}

Exception Types

Exceptions thrown during argument aggregation operations.

/**
 * Exception thrown when argument access fails
 */
@API(status = STABLE, since = "5.0")
class ArgumentAccessException extends JUnitException {
    
    ArgumentAccessException(String message) {
        super(message);
    }
    
    ArgumentAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

/**
 * Exception thrown when argument aggregation fails
 */
@API(status = STABLE, since = "5.0")
class ArgumentsAggregationException extends JUnitException {
    
    ArgumentsAggregationException(String message) {
        super(message);
    }
    
    ArgumentsAggregationException(String message, Throwable cause) {
        super(message, cause);
    }
}

DefaultArgumentsAccessor

Default implementation of ArgumentsAccessor interface.

/**
 * Default implementation of ArgumentsAccessor
 */
@API(status = STABLE, since = "5.0")
class DefaultArgumentsAccessor implements ArgumentsAccessor {
    
    private final Object[] arguments;
    private final int invocationIndex;
    
    /**
     * Creates accessor for given arguments and invocation index
     */
    DefaultArgumentsAccessor(Object[] arguments, int invocationIndex) {
        this.arguments = arguments;
        this.invocationIndex = invocationIndex;
    }
    
    // Implementation of all ArgumentsAccessor methods...
}

Complex Aggregation Examples

Map-based aggregation:

class MapAggregator implements ArgumentsAggregator {
    
    @Override
    public Map<String, Object> aggregateArguments(ArgumentsAccessor accessor, 
            ParameterContext context) {
        Map<String, Object> result = new HashMap<>();
        
        // Assume even indices are keys, odd indices are values
        for (int i = 0; i < accessor.size() - 1; i += 2) {
            String key = accessor.getString(i);
            Object value = accessor.get(i + 1);
            result.put(key, value);
        }
        
        return result;
    }
}

class MapAggregationExample {
    
    @ParameterizedTest
    @CsvSource({
        "name, Alice, age, 25, active, true",
        "name, Bob, age, 30, active, false"
    })
    void testMapAggregation(@AggregateWith(MapAggregator.class) Map<String, Object> data) {
        assertNotNull(data);
        assertTrue(data.containsKey("name"));
        assertTrue(data.containsKey("age"));
        assertTrue(data.containsKey("active"));
        
        String name = (String) data.get("name");
        assertNotNull(name);
    }
}

Validation during aggregation:

class ValidatingPersonAggregator implements ArgumentsAggregator {
    
    @Override
    public Person aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) 
            throws ArgumentsAggregationException {
        String name = accessor.getString(0);
        int age = accessor.getInteger(1);
        boolean active = accessor.getBoolean(2);
        
        // Validation logic
        if (name == null || name.trim().isEmpty()) {
            throw new ArgumentsAggregationException("Name cannot be empty");
        }
        
        if (age < 0 || age > 150) {
            throw new ArgumentsAggregationException("Invalid age: " + age);
        }
        
        return new Person(name, age, active);
    }
}

class ValidatingAggregationExample {
    
    @ParameterizedTest
    @CsvSource({
        "Alice, 25, true",
        "Bob, 30, false"
    })
    void testValidatingAggregation(@AggregateWith(ValidatingPersonAggregator.class) Person person) {
        assertNotNull(person);
        assertFalse(person.getName().trim().isEmpty());
        assertTrue(person.getAge() >= 0 && person.getAge() <= 150);
    }
}

The argument aggregation system provides powerful capabilities for combining multiple test arguments into cohesive objects, enabling clean test method signatures and complex parameter validation while maintaining type safety and clear error reporting.

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