Skip to content

Commit

Permalink
Add simple aggregator (#745 / #744)
Browse files Browse the repository at this point in the history
Add argument aggregator for simple use-cases.

Closes: #745
PR: #744
  • Loading branch information
Michael1993 committed Sep 7, 2023
1 parent c7bb969 commit 2f4feb7
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 5 deletions.
2 changes: 1 addition & 1 deletion docs/disable-parameterized-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The reason is that it's not clear from reading the annotation whether it's *and*
== DisableIfArgument

This extension can be used to selectively disable parameterized tests based on their arguments (converted with `toString()`).
The extension comes with three annotations, covering different use-cases:
The extension comes with three annotations, covering different use cases:

- `@DisableIfAnyArgument`, non-repeatable
- `@DisableIfAllArguments`, non-repeatable
Expand Down
2 changes: 2 additions & 0 deletions docs/docs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,7 @@
url: /docs/retrying-test/
- title: "Standard Input and Output"
url: /docs/standard-input-output/
- title: "Simple Arguments Aggregator"
url: /docs/simple-arguments-aggregator/
- title: "Vintage @Test"
url: /docs/vintage-test/
38 changes: 38 additions & 0 deletions docs/simple-arguments-aggregator.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
:page-title: Simple Arguments Aggregator
:page-description: The JUnit 5 (Jupiter) extension `@Aggregate` aggregates supplied values into a single parameter for a `@ParameterizedTest`
:xp-demo-dir: ../src/demo/java
:demo: {xp-demo-dir}/org/junitpioneer/jupiter/params/SimpleAggregatorDemo.java

Annotating a test parameter with `@Aggregate` aggregates all the supplied arguments into a single object.

== Usage

`@Aggregate` can be applied to a parameter in a `@ParameterizedTest`.

[source,java,indent=0]
====
include::{demo}[tag=basic_example]
====

== Limitations

The extension is meant to be used for simple use cases and has a couple of limitations.

- The parameter object must have a `public` constructor.
- The arguments must be in the same order as the constructor parameters.
- The parameter object must be non-composite - it can not have another object(s) as fields.

This last point has a few exceptions based on JUnit 5 support for https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-argument-conversion-implicit[implicit type conversions].
In the example above, if we have the following fields in the `Person` class:

[source,java,indent=0]
====
include::{demo}[tag=person_class]
====

Then JUnit 5 will take care of the conversion from `String` to `Gender` and `LocalDate`.
If you need to supply more complex objects to your tests, see if link:/docs/json-argument-source.adoc[JSON arguments sources] cover your use case.

== Thread-Safety

This extension is safe to use during https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution[parallel test execution].
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.params;

import java.time.LocalDate;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class SimpleAggregatorDemo {

// tag::basic_example[]
@ParameterizedTest
@CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" })
void test(@Aggregate Person person) {
}
// end::basic_example[]

static class Person {

// tag::person_class[]
private final String firstName;
private final String lastName;
private final Gender gender;
private final LocalDate birthday;
// end::person_class[]

public Person(String firstName, String lastName, Gender gender, LocalDate birthday) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.birthday = birthday;
}

}

enum Gender {
F, M, X
}

}
43 changes: 43 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/params/Aggregate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.params;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.params.aggregator.AggregateWith;

/**
* {@code @Aggregate} is a parameter annotation, used for simple argument aggregation.
*
* <p>The supplied values are expected to be able to be aggregated into a single argument,
* which is in turn supplied to the {@code @ParameterizedTest} method.</p>
*
* <p>For more details (including its limitations) and examples, see
* <a href="https://junit-pioneer.org/docs/simple-arguments-aggregator/" target="_top">the documentation on
* Simple Arguments Aggregator</a>
* </p>
*
* <p>This annotation is not compatible with {@link org.junitpioneer.jupiter.cartesian.CartesianTest} since
* this expects a single parameter as opposed to {@link org.junitpioneer.jupiter.cartesian.CartesianTest}
* requiring multiple parameters.
* </p>
*
* @since 2.1
* @see org.junit.jupiter.params.aggregator.ArgumentsAggregator
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AggregateWith(SimpleAggregator.class)
public @interface Aggregate {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.params;

import static java.lang.String.format;
import static java.util.stream.Collectors.toUnmodifiableSet;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
import org.junitpioneer.internal.PioneerUtils;

class SimpleAggregator implements ArgumentsAggregator {

public SimpleAggregator() {
// recreate default constructor to prevent compiler warning
}

@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
Class<?> type = context.getParameter().getType();
Set<Constructor<?>> constructors = Arrays
.stream(type.getConstructors())
// only if the constructor parameters and the supplied values are equal length
.filter(constructor -> constructor.getParameterCount() == accessor.size())
.collect(toUnmodifiableSet());
if (constructors.isEmpty())
throw new ArgumentsAggregationException(format(
"Could not aggregate arguments, no public constructor with %d parameters was found.", accessor.size()));
return tryEachConstructor(constructors, accessor);
}

private Object tryEachConstructor(Set<Constructor<?>> constructors, ArgumentsAccessor accessor) {
Object value = null;
List<Constructor<?>> matchingConstructors = new ArrayList<>();
for (Constructor<?> constructor : constructors) {
try {
Object[] arguments = new Object[accessor.size()];
for (int i = 0; i < accessor.size(); i++) {
// can't just check against types explicitly because JUnit might be able to convert to
// the types that we need, so we have to "force" that by using ArgumentsAccessor::get
// which invokes JUnit's built-in ArgumentConverter
// we also wrap primitive types to avoid casting problems - Java does auto unboxing later
arguments[i] = accessor.get(i, PioneerUtils.wrap(constructor.getParameterTypes()[i]));
}
value = constructor.newInstance(arguments);
matchingConstructors.add(constructor);
}
catch (Exception ignored) {
// continue, we throw an exception if no matching constructor is found
}
}
if (value == null)
throw new ArgumentsAggregationException(
"Could not aggregate arguments, no matching public constructor was found.");
if (matchingConstructors.size() > 1)
throw new ArgumentsAggregationException(
format("Could not aggregate arguments. Expected only one matching public constructor but found %d: %s",
matchingConstructors.size(), matchingConstructors));
return value;
}

}
11 changes: 7 additions & 4 deletions src/main/java/org/junitpioneer/jupiter/params/package-info.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* Several extensions for working with {@code ParameterizedTest}s.
* <p>
* Disable {@code @ParameterizedTest} executions based on conditions.</p>
* <p>Disable {@code @ParameterizedTest} executions based on conditions.</p>
* <p>Check out the following types for details:</p>
* <ul>
* <li>{@link org.junitpioneer.jupiter.params.DisableIfDisplayName}</li>
Expand All @@ -10,8 +9,7 @@
* <li>{@link org.junitpioneer.jupiter.params.DisableIfArgument}</li>
* </ul>
*
* <p>
* Argument providers for a range of numbers.</p>
* <p>Argument providers for a range of numbers.</p>
* <p>Check out the following types for details on providing values for parameterized tests:</p>
* <ul>
* <li>{@link org.junitpioneer.jupiter.params.ByteRangeSource}</li>
Expand All @@ -22,6 +20,11 @@
* <li>{@link org.junitpioneer.jupiter.params.DoubleRangeSource}</li>
* </ul>
*
* <p>Argument aggregator for simple use cases.</p>
* <p>Check out the following type for details:</p>
* <ul>
* <li>{@link org.junitpioneer.jupiter.params.Aggregate}</li>
* </ul>
*/

package org.junitpioneer.jupiter.params;

0 comments on commit 2f4feb7

Please sign in to comment.