Skip to content

Commit

Permalink
Add DynamicContainer support
Browse files Browse the repository at this point in the history
Prior to this commit only a flat collection of dynamically generated
tests was allowed as the return type of a @testfactory annotated
method. The collection entry type was set to `DynamicTest`.

This commit introduces the abstract base class `DynamicNode` and a
`DynamicContainer` class collecting node instance. @testfactory
annotated methods are now allowed to return a collection of nodes --
which can be a test or a named container of tests.
Creating a dynamic tree of tests like that gives users the ability to
better structure the generated tests.

The old behaviour, returning only instances of `DynamicTest`, is preserved.
  • Loading branch information
sormuras committed May 3, 2017
1 parent abfdc05 commit 1a57e4f
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 34 deletions.
26 changes: 26 additions & 0 deletions documentation/src/test/java/example/DynamicTestsDemo.java
Expand Up @@ -13,8 +13,10 @@
//tag::user_guide[]
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
Expand All @@ -24,9 +26,11 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.function.ThrowingConsumer;

class DynamicTestsDemo {
Expand All @@ -40,6 +44,28 @@ List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}

@TestFactory
List<DynamicNode> dynamicTestsWithContainers() {
Executable alwaysTrue = () -> assertTrue(true);

List<DynamicNode> nestedOne = new ArrayList<>();
nestedOne.add(dynamicTest("nested-1", alwaysTrue));
nestedOne.add(dynamicTest("nested-2", alwaysTrue));
List<DynamicNode> nestedTwo = new ArrayList<>();
nestedTwo.add(dynamicTest("nested-3", alwaysTrue));
nestedTwo.add(dynamicContainer("2nd level", dynamicTestsFromIterable()));
nestedTwo.add(dynamicTest("nested-4", alwaysTrue));
nestedTwo.add(dynamicContainer("level II", dynamicTestsFromStream()));

List<DynamicNode> nodes = new ArrayList<>();
nodes.add(dynamicTest("begin", alwaysTrue));
nodes.add(dynamicContainer("container one", nestedOne));
nodes.add(dynamicTest("middle", alwaysTrue));
nodes.add(dynamicContainer("container two", nestedTwo));
nodes.add(dynamicTest("end", alwaysTrue));
return nodes;
}

@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
// end::user_guide[]
Expand Down
@@ -0,0 +1,51 @@
/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.api;

import static org.junit.platform.commons.meta.API.Usage.Experimental;

import java.util.List;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.CollectionUtils;
import org.junit.platform.commons.util.Preconditions;

/**
* A {@code DynamicContainer} is a container generated at runtime.
*
* @since 5.0
*/
@API(Experimental)
public class DynamicContainer extends DynamicNode {

public static DynamicContainer dynamicContainer(String displayName, Iterable<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false));
}

public static DynamicContainer dynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, dynamicNodes);
}

private final List<DynamicNode> dynamicNodes;

private DynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
super(displayName);
Preconditions.notNull(dynamicNodes, "dynamicNodes must not be null");
this.dynamicNodes = dynamicNodes.collect(CollectionUtils.toUnmodifiableList());
Preconditions.containsNoNullElements(this.dynamicNodes, "individual dynamic node must not be null");
}

public Iterable<DynamicNode> getDynamicNodes() {
return dynamicNodes;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.api;

import static org.junit.platform.commons.meta.API.Usage.Experimental;

import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;

/**
* A {@code DynamicNode} is the abstract basis class for a container or a test
* case generated at runtime.
*
* @since 5.0
*/
@API(Experimental)
public abstract class DynamicNode {

private final String displayName;

DynamicNode(String displayName) {
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
}

/**
* Get the display name of this {@code DynamicTest}.
*/
public String getDisplayName() {
return this.displayName;
}

@Override
public String toString() {
return new ToStringBuilder(this).append("displayName", displayName).toString();
}

}
Expand Up @@ -23,7 +23,6 @@
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;

/**
* A {@code DynamicTest} is a test case generated at runtime.
Expand All @@ -46,7 +45,7 @@
* @see Executable
*/
@API(Experimental)
public class DynamicTest {
public class DynamicTest extends DynamicNode {

/**
* Factory for creating a new {@code DynamicTest} for the supplied display
Expand Down Expand Up @@ -99,31 +98,17 @@ public static <T> Stream<DynamicTest> stream(Iterator<T> inputGenerator,
// @formatter:on
}

private final String displayName;
private final Executable executable;

private DynamicTest(String displayName, Executable executable) {
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
super(displayName);
this.executable = Preconditions.notNull(executable, "executable must not be null");
}

/**
* Get the display name of this {@code DynamicTest}.
*/
public String getDisplayName() {
return this.displayName;
}

/**
* Get the {@code executable} code block associated with this {@code DynamicTest}.
*/
public Executable getExecutable() {
return this.executable;
}

@Override
public String toString() {
return new ToStringBuilder(this).append("displayName", displayName).toString();
}

}
@@ -0,0 +1,58 @@
/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.engine.descriptor;

import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.createDynamicDescriptor;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;

/**
* {@link TestDescriptor} for a {@link DynamicContainer}.
*
* @since 5.0
*/
class DynamicContainerTestDescriptor extends JupiterTestDescriptor {

private final DynamicContainer dynamicContainer;
private final TestSource testSource;

DynamicContainerTestDescriptor(UniqueId uniqueId, DynamicContainer dynamicContainer, TestSource testSource) {
super(uniqueId, dynamicContainer.getDisplayName());
this.dynamicContainer = dynamicContainer;
this.testSource = testSource;
setSource(testSource);
}

@Override
public Type getType() {
return Type.CONTAINER;
}

@Override
public boolean isLeaf() {
return true;
}

@Override
public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context,
DynamicTestExecutor dynamicTestExecutor) throws Exception {
int index = 1;
for (DynamicNode childNode : dynamicContainer.getDynamicNodes()) {
dynamicTestExecutor.execute(createDynamicDescriptor(this, childNode, index++, testSource));
}
return context;
}
}
Expand Up @@ -25,7 +25,7 @@ class DynamicTestTestDescriptor extends JupiterTestDescriptor {

private final DynamicTest dynamicTest;

public DynamicTestTestDescriptor(UniqueId uniqueId, DynamicTest dynamicTest, TestSource source) {
DynamicTestTestDescriptor(UniqueId uniqueId, DynamicTest dynamicTest, TestSource source) {
super(uniqueId, dynamicTest.getDisplayName());
this.dynamicTest = dynamicTest;
setSource(source);
Expand Down
Expand Up @@ -13,9 +13,11 @@
import static org.junit.platform.commons.meta.API.Usage.Internal;

import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Iterator;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.extension.TestExtensionContext;
import org.junit.jupiter.engine.execution.ExecutableInvoker;
Expand All @@ -24,18 +26,19 @@
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.CollectionUtils;
import org.junit.platform.commons.util.PreconditionViolationException;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;

/**
* {@link TestDescriptor} for {@link org.junit.jupiter.api.TestFactory @TestFactory}
* {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for {@link org.junit.jupiter.api.TestFactory @TestFactory}
* methods.
*
* @since 5.0
*/
@API(Internal)
public class TestFactoryTestDescriptor extends MethodTestDescriptor {

public static final String DYNAMIC_CONTAINER_SEGMENT_TYPE = "dynamic-container";
public static final String DYNAMIC_TEST_SEGMENT_TYPE = "dynamic-test";

private static final ExecutableInvoker executableInvoker = new ExecutableInvoker();
Expand Down Expand Up @@ -66,11 +69,15 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
Object instance = testExtensionContext.getTestInstance();
Object testFactoryMethodResult = executableInvoker.invoke(getTestMethod(), instance, testExtensionContext,
context.getExtensionRegistry());

try (Stream<DynamicTest> dynamicTestStream = toDynamicTestStream(testFactoryMethodResult)) {
AtomicInteger index = new AtomicInteger();
dynamicTestStream.forEach(
dynamicTest -> registerAndExecute(dynamicTest, index.incrementAndGet(), dynamicTestExecutor));
TestSource source = getSource().orElseThrow(() -> new JUnitException("Test source must be present"));
try (Stream<DynamicNode> dynamicNodeStream = toDynamicNodeStream(testFactoryMethodResult)) {
int index = 1;
Iterator<DynamicNode> iterator = dynamicNodeStream.iterator();
while (iterator.hasNext()) {
DynamicNode dynamicNode = iterator.next();
JupiterTestDescriptor descriptor = createDynamicDescriptor(this, dynamicNode, index++, source);
dynamicTestExecutor.execute(descriptor);
}
}
catch (ClassCastException ex) {
throw invalidReturnTypeException(ex);
Expand All @@ -79,26 +86,36 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
}

@SuppressWarnings("unchecked")
private Stream<DynamicTest> toDynamicTestStream(Object testFactoryMethodResult) {
private Stream<DynamicNode> toDynamicNodeStream(Object testFactoryMethodResult) {
try {
return (Stream<DynamicTest>) CollectionUtils.toStream(testFactoryMethodResult);
return (Stream<DynamicNode>) CollectionUtils.toStream(testFactoryMethodResult);
}
catch (PreconditionViolationException ex) {
throw invalidReturnTypeException(ex);
}
}

private void registerAndExecute(DynamicTest dynamicTest, int index, DynamicTestExecutor dynamicTestExecutor) {
UniqueId uniqueId = getUniqueId().append(DYNAMIC_TEST_SEGMENT_TYPE, "#" + index);
TestDescriptor descriptor = new DynamicTestTestDescriptor(uniqueId, dynamicTest, getSource().get());
addChild(descriptor);
dynamicTestExecutor.execute(descriptor);
static JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, int index,
TestSource source) {
JupiterTestDescriptor descriptor;
if (node instanceof DynamicTest) {
DynamicTest test = (DynamicTest) node;
UniqueId uniqueId = parent.getUniqueId().append(DYNAMIC_TEST_SEGMENT_TYPE, "#" + index);
descriptor = new DynamicTestTestDescriptor(uniqueId, test, source);
}
else {
DynamicContainer container = (DynamicContainer) node;
UniqueId uniqueId = parent.getUniqueId().append(DYNAMIC_CONTAINER_SEGMENT_TYPE, "#" + index);
descriptor = new DynamicContainerTestDescriptor(uniqueId, container, source);
}
parent.addChild(descriptor);
return descriptor;
}

private JUnitException invalidReturnTypeException(Throwable cause) {
String message = String.format(
"@TestFactory method [%s] must return a Stream, Collection, Iterable, or Iterator of %s.",
getTestMethod().toGenericString(), DynamicTest.class.getName());
getTestMethod().toGenericString(), DynamicNode.class.getName());
return new JUnitException(message, cause);
}

Expand Down

1 comment on commit 1a57e4f

@sbrannen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nicely done! 👍

Please sign in to comment.