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

Fix JSON sources to convert to Lists and other types #664

Merged
merged 14 commits into from Sep 26, 2022
7 changes: 7 additions & 0 deletions docs/json-argument-source.adoc
Expand Up @@ -79,6 +79,13 @@ This parametrized test will generate the following tests:
* [1] Luke
* [2] Yoda

The extension will automatically map JSON types to their corresponding Java types, including arrays:

[source,java,indent=0]
----
include::{json-demo}/[tag=inline_source_with_list]
----

=== Multiple Argument Methods

If the method has multiple arguments, each JSON object argument will be deconstructed to each of the method arguments.
Expand Down
Expand Up @@ -10,6 +10,8 @@

package org.junitpioneer.jupiter.json;

import java.util.List;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;

Expand Down Expand Up @@ -94,6 +96,20 @@ void singleJediProperty(
// end::inline_source_with_property[]
// @formatter:on

// @formatter:off
// tag::inline_source_with_list[]
@ParameterizedTest
@JsonSource({
"{ name: 'Yoda', padawans: ['Dooku', 'Luke'] }",
"{ name: 'Obi-Wan', padawans: ['Anakin', 'Luke'] }"
})
void multipleJedis(
@Property("padawans") List<String> padawanNames) {
// YOUR TEST CODE HERE
}
// end::inline_source_with_list[]
// @formatter:on

// @formatter:off
// tag::inline_source_deconstruct_from_array[]
@ParameterizedTest
Expand Down
Expand Up @@ -55,15 +55,16 @@ private static Object createArgumentForCartesianProvider(Parameter parameter, No
}

private static Arguments createArguments(Method method, Node node) {
boolean singleParameter = method.getParameterCount() == 1;
if (singleParameter) {
if (method.getParameterCount() == 1) {
Parameter onlyParameter = method.getParameters()[0];
// When there is a single parameter, the user might want to extract a single value or an entire type.
// When the parameter has the `@Property` annotation, then a single value needs to be extracted.
Property property = onlyParameter.getAnnotation(Property.class);
if (property == null) {
// no property specified -> the node should be converted in the parameter type
return Arguments.arguments(node.toType(onlyParameter.getType()));
// We must explicitly wrap the return into an Object[] because otherwise the return
// value is mistakenly interpreted as an Object[] and throws a ClassCastException
return () -> new Object[] { node.toType(onlyParameter.getParameterizedType()) };
}

// otherwise, treat this as method arguments
Expand All @@ -82,7 +83,7 @@ private static Arguments createArgumentsForMethod(Method method, Node node) {
: property.value();
return node
.getNode(name)
.map(value -> value.value(parameter.getType()))
.map(value -> value.value(parameter.getParameterizedType()))
.orElse(null);
})
.toArray();
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/org/junitpioneer/jupiter/json/JacksonNode.java
Expand Up @@ -11,6 +11,7 @@
package org.junitpioneer.jupiter.json;

import java.io.UncheckedIOException;
import java.lang.reflect.Type;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
Expand Down Expand Up @@ -43,9 +44,9 @@ public Stream<Node> elements() {
}

@Override
public <T> T toType(Class<T> type) {
public <T> T toType(Type type) {
try {
return objectMapper.treeToValue(node, type);
return objectMapper.treeToValue(node, objectMapper.constructType(type));
}
catch (JsonProcessingException e) {
throw new UncheckedIOException("Failed to convert to type " + type, e);
Expand All @@ -62,7 +63,7 @@ public Optional<Node> getNode(String name) {
}

@Override
public Object value(Class<?> typeHint) {
public Object value(Type typeHint) {
if (node.isTextual()) {
return node.textValue();
} else if (node.isInt()) {
Expand All @@ -81,7 +82,7 @@ public Object value(Class<?> typeHint) {
return node.decimalValue();
} else if (node.isBigInteger()) {
return node.bigIntegerValue();
} else if (node.isObject()) {
} else if (node.isObject() || node.isArray()) {
return toType(typeHint);
}
return node;
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/junitpioneer/jupiter/json/Node.java
Expand Up @@ -10,6 +10,7 @@

package org.junitpioneer.jupiter.json;

import java.lang.reflect.Type;
import java.util.Optional;
import java.util.stream.Stream;

Expand All @@ -36,7 +37,7 @@ interface Node {
* @param <T> the type
* @return the converted type
*/
<T> T toType(Class<T> type);
<T> T toType(Type type);

/**
* Get the node value with the given name.
Expand All @@ -52,6 +53,6 @@ interface Node {
* @param typeHint the potential type of the value
* @return the node value
*/
Object value(Class<?> typeHint);
Object value(Type typeHint);

}
169 changes: 169 additions & 0 deletions src/test/java/org/junitpioneer/jupiter/json/ArrayNodeToListTests.java
@@ -0,0 +1,169 @@
/*
* 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.json;

import static org.junitpioneer.testkit.assertion.PioneerAssert.assertThat;

import java.io.UncheckedIOException;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Predicate;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junitpioneer.jupiter.ReportEntry;
import org.junitpioneer.testkit.ExecutionResults;
import org.junitpioneer.testkit.PioneerTestKit;

@DisplayName("Converting JSON to lists")
public class ArrayNodeToListTests {

private static final String COMPOSERS = "org/junitpioneer/jupiter/json/composers.json";
private static final String POETS = "org/junitpioneer/jupiter/json/poets.json";
private static final String BAD_POEMS = "org/junitpioneer/jupiter/json/bad_poems.json";

@Test
@DisplayName("can convert classpath source arrays to List")
void classpathTest() {
ExecutionResults results = PioneerTestKit
.executeTestMethodWithParameterTypes(ArrayNodeToListTests.class, "convertFromClasspath", String.class,
List.class);

assertThat(results).hasNumberOfSucceededTests(2);
assertThat(results)
.hasNumberOfReportEntries(2)
.withValues("[Spartacus, Piano Concerto in D-flat major]",
"[The Isle of the Dead, Morceaux de fantaisie]");
}

@Test
@DisplayName("can convert annotation source arrays to list")
void annotationTest() {
ExecutionResults results = PioneerTestKit
.executeTestMethodWithParameterTypes(ArrayNodeToListTests.class, "convertFromAnnotation", List.class);

assertThat(results).hasNumberOfSucceededTests(2);
assertThat(results).hasNumberOfReportEntries(2).withValues("[1, 4, 7]", "[2, 4, 9]");
}

@Test
@DisplayName("can convert using a specific List implementation")
void specificImplementation() {
ExecutionResults results = PioneerTestKit
.executeTestMethodWithParameterTypes(ArrayNodeToListTests.class, "convertWithExplicitListType",
LinkedList.class);

assertThat(results).hasNumberOfSucceededTests(2);
assertThat(results).hasNumberOfReportEntries(2).withValues("[false, true, false]", "[true, false, true]");
}

@Test
@DisplayName("can convert classpath source arrays to List with more complex objects")
void classpathTestWithComplexObject() {
ExecutionResults results = PioneerTestKit
.executeTestMethodWithParameterTypes(ArrayNodeToListTests.class, "convertToComplexObject", String.class,
List.class);

assertThat(results).hasNumberOfSucceededTests(2);
assertThat(results)
.hasNumberOfReportEntries(2)
.withValues(
"Edgar Allan Poe: [The Black Cat (1843), The Cask of Amontillado (1846), The Pit and the Pendulum (1842)]",
"T. S. Eliot: [The Hollow Men (1925), Ash Wednesday (1930)]");
}

@Test
@DisplayName("throws a ParameterResolutionException if it can not convert complex objects")
void throwsForMalformedComplexObjects() {
ExecutionResults results = PioneerTestKit
.executeTestMethodWithParameterTypes(ArrayNodeToListTests.BadConfigurationTestCase.class,
"conversionException", List.class);

assertThat(results)
.hasSingleFailedContainer()
.withExceptionInstanceOf(UncheckedIOException.class)
.hasMessageContaining("Failed to convert to type");
}

@ParameterizedTest
@JsonClasspathSource(COMPOSERS)
@ReportEntry("{1}")
void convertFromClasspath(@Property("name") String name, @Property("music") List<String> works) {
Assertions.assertThat(name).isNotEmpty();
Assertions.assertThat(works).hasSize(2);
}

@ParameterizedTest
@JsonSource({ "{ 'single': [1, 4, 7] }", "{ 'single': [2, 4, 9] }" })
@ReportEntry("{0}")
void convertFromAnnotation(@Property("single") List<Integer> numbers) {
Assertions.assertThat(numbers).hasSize(3);
}

@ParameterizedTest
@JsonSource({ "{ 'statements': [true, false, true] }", "{ 'statements': [false, true, false] }" })
@ReportEntry("{0}")
void convertWithExplicitListType(@Property("statements") LinkedList<Boolean> numbers) {
Assertions.assertThat(numbers).hasSize(3);
}

@ParameterizedTest
@JsonClasspathSource(POETS)
@ReportEntry("{0}: {1}")
void convertToComplexObject(@Property("name") String name, @Property("works") List<Poem> poems) {
Predicate<Integer> predicate = name.equals("T. S. Eliot") ? release -> release < 1900
: release -> release > 1900;
Assertions.assertThat(poems).extracting(Poem::getRelease).noneMatch(predicate);
}

static class BadConfigurationTestCase {

@ParameterizedTest
@JsonClasspathSource(BAD_POEMS)
void conversionException(@Property("poems") List<Poem> poems) {
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
}

}

public static class Poem {

private String title;
private int release;

public Poem() {
}

public int getRelease() {
return release;
}

public void setRelease(int release) {
this.release = release;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

@Override
public String toString() {
return title + " (" + release + ")";
}

}

}
15 changes: 15 additions & 0 deletions src/test/resources/org/junitpioneer/jupiter/json/bad_poems.json
@@ -0,0 +1,15 @@
[
{
"name": "Robert Frost",
"poems": [
{
"title": "The Road Not Taken",
"release": 1915
},
{
"title": "A Witness Tree",
"release": "abcd"
}
]
}
]
16 changes: 16 additions & 0 deletions src/test/resources/org/junitpioneer/jupiter/json/composers.json
@@ -0,0 +1,16 @@
[
{
"name": "Aram Khachaturian",
"music": [
"Spartacus",
"Piano Concerto in D-flat major"
]
},
{
"name": "Sergei Rachmaninoff",
"music": [
"The Isle of the Dead",
"Morceaux de fantaisie"
]
}
]
32 changes: 32 additions & 0 deletions src/test/resources/org/junitpioneer/jupiter/json/poets.json
@@ -0,0 +1,32 @@
[
{
"name": "Edgar Allan Poe",
"works": [
{
"title": "The Black Cat",
"release": 1843
},
{
"title": "The Cask of Amontillado",
"release": 1846
},
{
"title": "The Pit and the Pendulum",
"release": 1842
}
]
},
{
"name": "T. S. Eliot",
"works": [
{
"title": "The Hollow Men",
"release": 1925
},
{
"title": "Ash Wednesday",
"release": 1930
}
]
}
]