Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple aggregator #744

Merged
merged 9 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -37,5 +37,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.
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
*
* 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.
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
*
* 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.
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 {
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
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]));
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
}
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;