0
# Argument Aggregation
1
2
System for combining multiple arguments into single parameter objects with type-safe access and custom aggregation logic.
3
4
## Capabilities
5
6
### @AggregateWith Annotation
7
8
Specifies ArgumentsAggregator for combining multiple arguments into a single parameter.
9
10
```java { .api }
11
/**
12
* Specifies an ArgumentsAggregator for combining multiple arguments
13
*/
14
@Target(ElementType.PARAMETER)
15
@Retention(RetentionPolicy.RUNTIME)
16
@Documented
17
@API(status = STABLE, since = "5.0")
18
@interface AggregateWith {
19
/**
20
* ArgumentsAggregator implementation class
21
*/
22
Class<? extends ArgumentsAggregator> value();
23
}
24
```
25
26
### ArgumentsAccessor Interface
27
28
Type-safe access to argument arrays with automatic conversion support.
29
30
```java { .api }
31
/**
32
* Provides type-safe access to arguments with automatic conversion
33
*/
34
@API(status = STABLE, since = "5.0")
35
interface ArgumentsAccessor {
36
37
/**
38
* Gets argument at index as Object
39
*/
40
Object get(int index);
41
42
/**
43
* Gets argument at index with type conversion
44
*/
45
<T> T get(int index, Class<T> requiredType);
46
47
/**
48
* Gets argument as Character
49
*/
50
Character getCharacter(int index);
51
52
/**
53
* Gets argument as Boolean
54
*/
55
Boolean getBoolean(int index);
56
57
/**
58
* Gets argument as Byte
59
*/
60
Byte getByte(int index);
61
62
/**
63
* Gets argument as Short
64
*/
65
Short getShort(int index);
66
67
/**
68
* Gets argument as Integer
69
*/
70
Integer getInteger(int index);
71
72
/**
73
* Gets argument as Long
74
*/
75
Long getLong(int index);
76
77
/**
78
* Gets argument as Float
79
*/
80
Float getFloat(int index);
81
82
/**
83
* Gets argument as Double
84
*/
85
Double getDouble(int index);
86
87
/**
88
* Gets argument as String
89
*/
90
String getString(int index);
91
92
/**
93
* Returns number of arguments
94
*/
95
int size();
96
97
/**
98
* Converts all arguments to Object array
99
*/
100
Object[] toArray();
101
102
/**
103
* Converts all arguments to List
104
*/
105
List<Object> toList();
106
107
/**
108
* Gets current invocation index (0-based)
109
*/
110
int getInvocationIndex();
111
}
112
```
113
114
### ArgumentsAggregator Interface
115
116
Contract for aggregating multiple arguments into a single object.
117
118
```java { .api }
119
/**
120
* Contract for aggregating multiple arguments into a single object
121
* Implementations must be thread-safe and have a no-args constructor
122
* or a single constructor whose parameters can be resolved by JUnit
123
*/
124
@API(status = STABLE, since = "5.0")
125
@FunctionalInterface
126
interface ArgumentsAggregator {
127
128
/**
129
* Aggregates multiple arguments into single object
130
*
131
* @param accessor type-safe access to arguments
132
* @param context parameter context for target type information
133
* @return aggregated object, may be null
134
* @throws ArgumentsAggregationException if aggregation fails
135
*/
136
Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
137
throws ArgumentsAggregationException;
138
}
139
```
140
141
**Usage Examples:**
142
143
```java
144
import org.junit.jupiter.params.ParameterizedTest;
145
import org.junit.jupiter.params.aggregator.AggregateWith;
146
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
147
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
148
import org.junit.jupiter.params.provider.CsvSource;
149
import org.junit.jupiter.api.extension.ParameterContext;
150
151
// Person class for aggregation examples
152
class Person {
153
private final String name;
154
private final int age;
155
private final boolean active;
156
157
Person(String name, int age, boolean active) {
158
this.name = name;
159
this.age = age;
160
this.active = active;
161
}
162
163
// Getters
164
public String getName() { return name; }
165
public int getAge() { return age; }
166
public boolean isActive() { return active; }
167
}
168
169
// Custom aggregator implementation
170
class PersonAggregator implements ArgumentsAggregator {
171
172
@Override
173
public Person aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) {
174
return new Person(
175
accessor.getString(0),
176
accessor.getInteger(1),
177
accessor.getBoolean(2)
178
);
179
}
180
}
181
182
class ArgumentAggregationExamples {
183
184
// Direct ArgumentsAccessor usage
185
@ParameterizedTest
186
@CsvSource({
187
"Alice, 25, true",
188
"Bob, 30, false",
189
"Charlie, 35, true"
190
})
191
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
192
String name = arguments.getString(0);
193
int age = arguments.getInteger(1);
194
boolean active = arguments.getBoolean(2);
195
196
assertNotNull(name);
197
assertTrue(age > 0);
198
199
// Can also access by type conversion
200
String ageString = arguments.get(1, String.class);
201
assertEquals(String.valueOf(age), ageString);
202
}
203
204
// Custom aggregator usage
205
@ParameterizedTest
206
@CsvSource({
207
"Alice, 25, true",
208
"Bob, 30, false",
209
"Charlie, 35, true"
210
})
211
void testWithCustomAggregator(@AggregateWith(PersonAggregator.class) Person person) {
212
assertNotNull(person);
213
assertNotNull(person.getName());
214
assertTrue(person.getAge() > 0);
215
}
216
217
// Mixed parameters with aggregation
218
@ParameterizedTest
219
@CsvSource({
220
"1, Alice, 25, true",
221
"2, Bob, 30, false"
222
})
223
void testMixedParameters(int id, @AggregateWith(PersonAggregator.class) Person person) {
224
assertTrue(id > 0);
225
assertNotNull(person);
226
assertNotNull(person.getName());
227
}
228
}
229
```
230
231
### Advanced Aggregation Patterns
232
233
**Generic aggregator for any class:**
234
235
```java
236
import java.lang.reflect.Constructor;
237
238
class ReflectiveAggregator implements ArgumentsAggregator {
239
240
@Override
241
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
242
throws ArgumentsAggregationException {
243
try {
244
Class<?> targetType = context.getParameter().getType();
245
Constructor<?> constructor = findMatchingConstructor(targetType, accessor.size());
246
247
Object[] args = new Object[accessor.size()];
248
Class<?>[] paramTypes = constructor.getParameterTypes();
249
250
for (int i = 0; i < accessor.size(); i++) {
251
args[i] = accessor.get(i, paramTypes[i]);
252
}
253
254
return constructor.newInstance(args);
255
} catch (Exception e) {
256
throw new ArgumentsAggregationException("Aggregation failed", e);
257
}
258
}
259
260
private Constructor<?> findMatchingConstructor(Class<?> clazz, int argCount) {
261
for (Constructor<?> constructor : clazz.getConstructors()) {
262
if (constructor.getParameterCount() == argCount) {
263
return constructor;
264
}
265
}
266
throw new RuntimeException("No matching constructor found");
267
}
268
}
269
270
class Product {
271
private final String name;
272
private final double price;
273
private final boolean available;
274
275
public Product(String name, double price, boolean available) {
276
this.name = name;
277
this.price = price;
278
this.available = available;
279
}
280
281
// Getters
282
public String getName() { return name; }
283
public double getPrice() { return price; }
284
public boolean isAvailable() { return available; }
285
}
286
287
class ReflectiveAggregationExample {
288
289
@ParameterizedTest
290
@CsvSource({
291
"Apple, 1.50, true",
292
"Banana, 0.75, false"
293
})
294
void testReflectiveAggregation(@AggregateWith(ReflectiveAggregator.class) Product product) {
295
assertNotNull(product);
296
assertNotNull(product.getName());
297
assertTrue(product.getPrice() >= 0);
298
}
299
}
300
```
301
302
**Builder pattern aggregator:**
303
304
```java
305
class UserBuilder {
306
private String name;
307
private int age;
308
private String email;
309
private boolean active = true;
310
311
public UserBuilder name(String name) {
312
this.name = name;
313
return this;
314
}
315
316
public UserBuilder age(int age) {
317
this.age = age;
318
return this;
319
}
320
321
public UserBuilder email(String email) {
322
this.email = email;
323
return this;
324
}
325
326
public UserBuilder active(boolean active) {
327
this.active = active;
328
return this;
329
}
330
331
public User build() {
332
return new User(name, age, email, active);
333
}
334
}
335
336
class User {
337
private final String name, email;
338
private final int age;
339
private final boolean active;
340
341
User(String name, int age, String email, boolean active) {
342
this.name = name;
343
this.age = age;
344
this.email = email;
345
this.active = active;
346
}
347
348
// Getters
349
public String getName() { return name; }
350
public String getEmail() { return email; }
351
public int getAge() { return age; }
352
public boolean isActive() { return active; }
353
}
354
355
class UserBuilderAggregator implements ArgumentsAggregator {
356
357
@Override
358
public User aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) {
359
return new UserBuilder()
360
.name(accessor.getString(0))
361
.age(accessor.getInteger(1))
362
.email(accessor.getString(2))
363
.active(accessor.getBoolean(3))
364
.build();
365
}
366
}
367
368
class BuilderAggregationExample {
369
370
@ParameterizedTest
371
@CsvSource({
372
"Alice, 25, alice@example.com, true",
373
"Bob, 30, bob@example.com, false"
374
})
375
void testBuilderAggregation(@AggregateWith(UserBuilderAggregator.class) User user) {
376
assertNotNull(user);
377
assertNotNull(user.getName());
378
assertTrue(user.getEmail().contains("@"));
379
assertTrue(user.getAge() > 0);
380
}
381
}
382
```
383
384
### Exception Types
385
386
Exceptions thrown during argument aggregation operations.
387
388
```java { .api }
389
/**
390
* Exception thrown when argument access fails
391
*/
392
@API(status = STABLE, since = "5.0")
393
class ArgumentAccessException extends JUnitException {
394
395
ArgumentAccessException(String message) {
396
super(message);
397
}
398
399
ArgumentAccessException(String message, Throwable cause) {
400
super(message, cause);
401
}
402
}
403
404
/**
405
* Exception thrown when argument aggregation fails
406
*/
407
@API(status = STABLE, since = "5.0")
408
class ArgumentsAggregationException extends JUnitException {
409
410
ArgumentsAggregationException(String message) {
411
super(message);
412
}
413
414
ArgumentsAggregationException(String message, Throwable cause) {
415
super(message, cause);
416
}
417
}
418
```
419
420
### DefaultArgumentsAccessor
421
422
Default implementation of ArgumentsAccessor interface.
423
424
```java { .api }
425
/**
426
* Default implementation of ArgumentsAccessor
427
*/
428
@API(status = STABLE, since = "5.0")
429
class DefaultArgumentsAccessor implements ArgumentsAccessor {
430
431
private final Object[] arguments;
432
private final int invocationIndex;
433
434
/**
435
* Creates accessor for given arguments and invocation index
436
*/
437
DefaultArgumentsAccessor(Object[] arguments, int invocationIndex) {
438
this.arguments = arguments;
439
this.invocationIndex = invocationIndex;
440
}
441
442
// Implementation of all ArgumentsAccessor methods...
443
}
444
```
445
446
### Complex Aggregation Examples
447
448
**Map-based aggregation:**
449
450
```java
451
class MapAggregator implements ArgumentsAggregator {
452
453
@Override
454
public Map<String, Object> aggregateArguments(ArgumentsAccessor accessor,
455
ParameterContext context) {
456
Map<String, Object> result = new HashMap<>();
457
458
// Assume even indices are keys, odd indices are values
459
for (int i = 0; i < accessor.size() - 1; i += 2) {
460
String key = accessor.getString(i);
461
Object value = accessor.get(i + 1);
462
result.put(key, value);
463
}
464
465
return result;
466
}
467
}
468
469
class MapAggregationExample {
470
471
@ParameterizedTest
472
@CsvSource({
473
"name, Alice, age, 25, active, true",
474
"name, Bob, age, 30, active, false"
475
})
476
void testMapAggregation(@AggregateWith(MapAggregator.class) Map<String, Object> data) {
477
assertNotNull(data);
478
assertTrue(data.containsKey("name"));
479
assertTrue(data.containsKey("age"));
480
assertTrue(data.containsKey("active"));
481
482
String name = (String) data.get("name");
483
assertNotNull(name);
484
}
485
}
486
```
487
488
**Validation during aggregation:**
489
490
```java
491
class ValidatingPersonAggregator implements ArgumentsAggregator {
492
493
@Override
494
public Person aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
495
throws ArgumentsAggregationException {
496
String name = accessor.getString(0);
497
int age = accessor.getInteger(1);
498
boolean active = accessor.getBoolean(2);
499
500
// Validation logic
501
if (name == null || name.trim().isEmpty()) {
502
throw new ArgumentsAggregationException("Name cannot be empty");
503
}
504
505
if (age < 0 || age > 150) {
506
throw new ArgumentsAggregationException("Invalid age: " + age);
507
}
508
509
return new Person(name, age, active);
510
}
511
}
512
513
class ValidatingAggregationExample {
514
515
@ParameterizedTest
516
@CsvSource({
517
"Alice, 25, true",
518
"Bob, 30, false"
519
})
520
void testValidatingAggregation(@AggregateWith(ValidatingPersonAggregator.class) Person person) {
521
assertNotNull(person);
522
assertFalse(person.getName().trim().isEmpty());
523
assertTrue(person.getAge() >= 0 && person.getAge() <= 150);
524
}
525
}
526
```
527
528
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.