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

Support JSON as source for parametrized tests arguments #492

Merged
merged 18 commits into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions .infra/checkstyle/import-control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,15 @@
<import-control pkg="org.junitpioneer">

<allow pkg=".*" regex="true" />
<disallow pkg="com.fasterxml.jackson.*" regex="true"/>

<subpackage name="jupiter.json">
<file name="JacksonNode.java">
<allow pkg="com.fasterxml.jackson.*" regex="true" />
</file>
<file name="JacksonJsonConverter.java">
<allow pkg="com.fasterxml.jackson.*" regex="true" />
</file>
</subpackage>

</import-control>
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ java {
}
withJavadocJar()
withSourcesJar()
registerFeature("jackson") {
usingSourceSet(sourceSets["main"])
}
}

repositories {
Expand All @@ -58,6 +61,7 @@ dependencies {
implementation(group = "org.junit.jupiter", name = "junit-jupiter-params")
implementation(group = "org.junit.platform", name = "junit-platform-commons")
implementation(group = "org.junit.platform", name = "junit-platform-launcher")
"jacksonImplementation"(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = "2.13.2")

testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-engine")
testImplementation(group = "org.junit.platform", name = "junit-platform-testkit")
Expand Down
192 changes: 192 additions & 0 deletions docs/json-argument-source.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
:page-title: JSON Argument Source
filiphr marked this conversation as resolved.
Show resolved Hide resolved
:page-description: Extends JUnit Jupiter with `@JsonFileSource`, a parametrized test that creates test based on a JSON Source

The JSON argument sources let you provide arguments for parameterized tests from JSON.
There are three annotations:

* `@JsonSource` for lenient inline JSON
* `@JsonFileSource` for JSON files from the local file system
* `JsonClasspathSource` for JSON files from the classpath

There are various ways how the method arguments for a single parametrized test are provided.
By default the root of the source will be treated as candidate for the test arguments.
If the root is an object then the entire object will be one argument, if the root is an array then every element of the array will be one argument.

It is also possible to use a nested array from the provided JSON to access the source for the test arguments.
The `JsonFileSource#data` can be used to tell the extraction mechanism to use the element with that name to look for the source of the data.

Depending on the test method parameters, the extraction of the values might differ.

== Method Arguments

=== Single Argument Methods

If the method has a single argument, the JSON object argument will be converted to that type.

.Argument type
[source,java]
----
public class Jedi {
public String name;
public String height;

@Override
public String toString() {
return "Jedi {" + "name='" + name + '\'' + ", height=" + height + '}';
}
}
----

.JSON Source File
[source,json]
----
[
{
"name": "Luke",
"height": 172
},
{
"name": "Yoda",
"height": 66
}
]
----

[source,java]
----
@ParametrizedTest
@JsonFileSource(resources = "/jedis.json")
void singleJedi(Jedi jedi) {
// Assertions
}
----

[source,java]
----
@ParametrizedTest
@JsonSource("["
+ " { name: 'Luke', height: 172 },"
+ " { name: 'Yoda', height: 66 }"
+ "]")
void singleJedi(Jedi jedi) {
// Assertions
}
----

This parametrized test will generate the following test executions:

* [1] Jedi {name='Luke', height=172}
* [2] Jedi {name='Yoda', height=66}

It is also possible to extract only a single element from each argument object by using the `@Property` annotation.

[source,java]
----
@ParametrizedTest
@JsonFileSource(resources = "/jedis.json")
void singleJedi(@Property("name") String jediName) {
// Assertions
}
----

[source,java]
----
@ParametrizedTest
@JsonSource({
"{ name: 'Luke', height: 172 }",
"{ name: 'Yoda', height: 66 }"
})
void singleJedi(@Property("name") String jediName) {
// Assertions
}
----

This parametrized test will generate the following tests:

* [1] Luke
* [2] Yoda

=== Multiple Argument Methods

If the method has multiple arguments, each JSON object argument will be deconstructed to each of the method arguments.
By default, the method argument name will be used for locating the element that needs to be taken from the JSON object.
You can also use `@Property` to give the name of the element that needs to be extracted.

[IMPORTANT]
====
If your test sources are not compiled using the `--parameters` flag then the names of the arguments will not be like they are written in the source code.
In that the situation you need to use `@Property` instead.
====

Using the same `jedis.json` and the following test

[source,java]
----
@ParametrizedTest
@JsonFileSource(resources = "/jedis.json")
void deconstructFromArray(@Property("name") String name, @Property("height") int height) {
// Assertions
}
----

[source,java]
----
@ParametrizedTest
@JsonSource({
"{ name: 'Yoda', height: 66 }",
"{ name: 'Luke', height: 172 }",
})
void deconstructFromArray(@Property("name") String name, @Property("height") int height) {
// Assertions
}
----

This parametrized test will generate the following tests:

* [1] Luke, 172
* [2] Yoda, 66

== Extracting nested array

Sometimes we want to extract a nested array instead of the root element.
For this purpose `JsonFileSource#data` can be used.

.Jedi with nested array
[source,json]
----
{
"name": "Luke",
"height": 172,
"vehicles": [
{
"name": "Snowspeeder",
"length": 4.5
},
{
"name": "Imperial Speeder Bike",
"length": 3
}
]
}
----

Here we want to test the vehicles.
The test for this will look like:

[source,java]
----
@ParameterizedTest
@JsonFileSource(resources = "luke.json", data = "vehicles")
void lukeVehicles(@Property("name") String name, @Property("length") double length) {
// Assertions
}
----

This parametrized test will generate the following tests:

* [1] Snowspeeder, 4.5
* [2] Imperial Speeder Bike, 3

== Thread-Safety

This extension is safe to use during https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution[parallel test execution].
81 changes: 81 additions & 0 deletions src/main/java/org/junitpioneer/internal/PioneerPreconditions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2016-2021 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.internal;

import java.util.Collection;
import java.util.function.Supplier;

import org.junit.platform.commons.PreconditionViolationException;

/**
* Pioneer-internal utility class to handle preconditions.
* DO NOT USE THIS CLASS - IT MAY CHANGE SIGNIFICANTLY IN ANY MINOR UPDATE.
*/
public class PioneerPreconditions {

private PioneerPreconditions() {
// private constructor to prevent instantiation of utility class
}

/**
* Assert that the supplied string is not blank.
* @param str the string to check
* @param messageSupplier the precondition violation message supplier
* @return the supplied string
*/
public static String notBlank(String str, Supplier<String> messageSupplier) {
if (str == null || str.trim().isEmpty()) {
throw new PreconditionViolationException(messageSupplier.get());
}

return str;
}

/**
* Assert that the supplied object is not null.
* @param object the object to check
* @param messageSupplier the precondition violation message supplier
* @return the supplied object
*/
public static <T> T notNull(T object, Supplier<String> messageSupplier) {
if (object == null) {
throw new PreconditionViolationException(messageSupplier.get());
}
return object;
}

/**
* Assert that the supplied collection is not empty.
* @param collection the collection to check
* @param messageSupplier the precondition violation message supplier
* @return the supplied string
*/
public static <T extends Collection<?>> T notEmpty(T collection, Supplier<String> messageSupplier) {
if (collection == null || collection.isEmpty()) {
throw new PreconditionViolationException(messageSupplier.get());
}
return collection;
}

/**
* Assert that the supplied collection is not empty.
* @param collection the collection to check
* @param message the precondition violation message supplier
* @return the supplied string
*/
public static <T extends Collection<?>> T notEmpty(T collection, String message) {
if (collection == null || collection.isEmpty()) {
throw new PreconditionViolationException(message);
}
return collection;
}

}