From 1a57e4f386a9f8cd6be0a08a4d914349d32a7f01 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Fri, 28 Apr 2017 01:49:44 +0200 Subject: [PATCH] Add DynamicContainer support 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. --- .../test/java/example/DynamicTestsDemo.java | 26 +++++++++ .../junit/jupiter/api/DynamicContainer.java | 51 ++++++++++++++++ .../org/junit/jupiter/api/DynamicNode.java | 46 +++++++++++++++ .../org/junit/jupiter/api/DynamicTest.java | 19 +----- .../DynamicContainerTestDescriptor.java | 58 +++++++++++++++++++ .../descriptor/DynamicTestTestDescriptor.java | 2 +- .../descriptor/TestFactoryTestDescriptor.java | 49 +++++++++++----- 7 files changed, 217 insertions(+), 34 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java diff --git a/documentation/src/test/java/example/DynamicTestsDemo.java b/documentation/src/test/java/example/DynamicTestsDemo.java index c90b8adfb3d..71e495cbfa9 100644 --- a/documentation/src/test/java/example/DynamicTestsDemo.java +++ b/documentation/src/test/java/example/DynamicTestsDemo.java @@ -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; @@ -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 { @@ -40,6 +44,28 @@ List dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } + @TestFactory + List dynamicTestsWithContainers() { + Executable alwaysTrue = () -> assertTrue(true); + + List nestedOne = new ArrayList<>(); + nestedOne.add(dynamicTest("nested-1", alwaysTrue)); + nestedOne.add(dynamicTest("nested-2", alwaysTrue)); + List 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 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 dynamicTestsFromCollection() { // end::user_guide[] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java new file mode 100644 index 00000000000..f5ccfac84c4 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java @@ -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 dynamicNodes) { + return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false)); + } + + public static DynamicContainer dynamicContainer(String displayName, Stream dynamicNodes) { + return new DynamicContainer(displayName, dynamicNodes); + } + + private final List dynamicNodes; + + private DynamicContainer(String displayName, Stream 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 getDynamicNodes() { + return dynamicNodes; + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java new file mode 100644 index 00000000000..51ed5953d40 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java @@ -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(); + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java index e48d89e17c2..fe5e927dc69 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java @@ -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. @@ -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 @@ -99,31 +98,17 @@ public static Stream stream(Iterator 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(); - } - } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java new file mode 100644 index 00000000000..aeecb9ebc72 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java @@ -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; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java index 5f987118ddb..0186373038a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java @@ -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); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java index 98aaf5af7a4..afb5280c176 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java @@ -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; @@ -24,11 +26,11 @@ 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 @@ -36,6 +38,7 @@ @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(); @@ -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 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 dynamicNodeStream = toDynamicNodeStream(testFactoryMethodResult)) { + int index = 1; + Iterator 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); @@ -79,26 +86,36 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe } @SuppressWarnings("unchecked") - private Stream toDynamicTestStream(Object testFactoryMethodResult) { + private Stream toDynamicNodeStream(Object testFactoryMethodResult) { try { - return (Stream) CollectionUtils.toStream(testFactoryMethodResult); + return (Stream) 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); }