diff --git a/documentation/src/test/java/example/DynamicTestsDemo.java b/documentation/src/test/java/example/DynamicTestsDemo.java index 088c897073eb..58ccfc1abbea 100644 --- a/documentation/src/test/java/example/DynamicTestsDemo.java +++ b/documentation/src/test/java/example/DynamicTestsDemo.java @@ -28,6 +28,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicRuntime; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestFactory; @@ -44,6 +45,48 @@ List dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } + @TestFactory + DynamicNode[] dynamicNodesWithRequiredTests() { + // end::user_guide[] + // @formatter:off + // tag::user_guide[] + return new DynamicNode[] { + dynamicTest("Visit page requiring authorization while not logged in", () -> { + // attempt to visit page which requires that a user is logged in + // assert user is redirected to login page + }), + dynamicTest("Log-in", () -> { + // submit login form with valid credentials + // assert user is redirected back to previous page requiring authorization + // fail("you shall not pass"); + }, + // if login failed return "false" to break the dynamic execution loop here + DynamicRuntime::wasLastExecutableSuccessful + ), + dynamicContainer("Can access several pages while logged in", + dynamicTest("Visit second page requiring authorization while logged in", () -> { + // visit another page which requires that a user is logged in + // assert user can access page + }), + dynamicTest("Visit third page requiring authorization while logged in", () -> { + // visit another page which requires that a user is logged in + // assert user can access page + }), + dynamicTest("Visit fourth page requiring authorization while logged in", () -> { + // visit another page which requires that a user is logged in + // assert user can access page + }) + ), + dynamicTest("Log-out", () -> { + // visit logout URL + // assert user has been logged out + }) + }; + // end::user_guide[] + // @formatter:on + // tag::user_guide[] + } + @TestFactory Stream dynamicTestsWithContainers() { // 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 index f5ccfac84c4d..20008e46fb3e 100644 --- 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 @@ -12,6 +12,7 @@ import static org.junit.platform.commons.meta.API.Usage.Experimental; +import java.util.Arrays; import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -28,6 +29,10 @@ @API(Experimental) public class DynamicContainer extends DynamicNode { + public static DynamicContainer dynamicContainer(String displayName, DynamicNode... dynamicNodes) { + return new DynamicContainer(displayName, Arrays.stream(dynamicNodes)); + } + public static DynamicContainer dynamicContainer(String displayName, Iterable dynamicNodes) { return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false)); } @@ -39,13 +44,18 @@ public static DynamicContainer dynamicContainer(String displayName, Stream dynamicNodes; private DynamicContainer(String displayName, Stream dynamicNodes) { - super(displayName); + super(displayName, stayAlive -> true); 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"); + Preconditions.notEmpty(this.dynamicNodes, "dynamic node collection passed to container must not be empty"); } + /** + * Get the dynamic child nodes. + */ 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 index 51ed5953d402..070397dad034 100644 --- 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 @@ -12,6 +12,8 @@ import static org.junit.platform.commons.meta.API.Usage.Experimental; +import java.util.function.Predicate; + import org.junit.platform.commons.meta.API; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ToStringBuilder; @@ -26,21 +28,27 @@ public abstract class DynamicNode { private final String displayName; + private final Predicate stayAlive; - DynamicNode(String displayName) { + DynamicNode(String displayName, Predicate stayAlive) { this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank"); + this.stayAlive = Preconditions.notNull(stayAlive, "stayAlive predicate must not be null"); } /** - * Get the display name of this {@code DynamicTest}. + * Get the display name of this {@code DynamicNode}. */ public String getDisplayName() { return this.displayName; } + public boolean breaking(DynamicRuntime runtimeInformation) { + return !stayAlive.test(runtimeInformation); + } + @Override public String toString() { - return new ToStringBuilder(this).append("displayName", displayName).toString(); + return new ToStringBuilder(this).append("displayName", displayName).append("stayAlive", stayAlive).toString(); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicRuntime.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicRuntime.java new file mode 100644 index 000000000000..e765c78d5814 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicRuntime.java @@ -0,0 +1,30 @@ +/* + * 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.time.Instant; + +import org.junit.platform.commons.meta.API; + +/** + * Dynamic runtime information. + * + * @since 5.0 + */ +@API(Experimental) +public interface DynamicRuntime { + + Instant getInstantOfTestFactoryStart(); + + boolean wasLastExecutableSuccessful(); +} 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 fe5e927dc698..929630a13d4d 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 @@ -16,6 +16,7 @@ import java.util.Iterator; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -51,14 +52,35 @@ public class DynamicTest extends DynamicNode { * Factory for creating a new {@code DynamicTest} for the supplied display * name and executable code block. * + *

An unsuccessful execution result does not break the execution loop. + * * @param displayName the display name for the dynamic test; never * {@code null} or blank * @param executable the executable code block for the dynamic test; * never {@code null} + * @see #dynamicTest(String, Executable, Predicate) * @see #stream(Iterator, Function, ThrowingConsumer) */ public static DynamicTest dynamicTest(String displayName, Executable executable) { - return new DynamicTest(displayName, executable); + return new DynamicTest(displayName, executable, __ -> true); + } + + /** + * Factory for creating a new required {@code DynamicTest} for the supplied + * display name and executable code block. + * + *

It is up to the user-supplied predicate whether the execution loop breaks. + * + * @param displayName the display name for the dynamic test; never + * {@code null} or blank + * @param executable the executable code block for the dynamic test; + * never {@code null} + * @see #dynamicTest(String, Executable) + * @see #stream(Iterator, Function, ThrowingConsumer) + */ + public static DynamicTest dynamicTest(String displayName, Executable executable, + Predicate stayAlive) { + return new DynamicTest(displayName, executable, stayAlive); } /** @@ -100,8 +122,8 @@ public static Stream stream(Iterator inputGenerator, private final Executable executable; - private DynamicTest(String displayName, Executable executable) { - super(displayName); + private DynamicTest(String displayName, Executable executable, Predicate stayAlive) { + super(displayName, stayAlive); this.executable = Preconditions.notNull(executable, "executable must not be null"); } @@ -111,4 +133,5 @@ private DynamicTest(String displayName, Executable executable) { public Executable getExecutable() { return this.executable; } + } 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 index 6f2a8ca7b928..259ac03c8935 100644 --- 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 @@ -10,12 +10,13 @@ package org.junit.jupiter.engine.descriptor; -import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.createDynamicDescriptor; +import java.util.Optional; 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.TestExecutionResult; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; @@ -26,11 +27,14 @@ */ class DynamicContainerTestDescriptor extends JupiterTestDescriptor { + private final TestFactoryTestDescriptor root; private final DynamicContainer dynamicContainer; private final TestSource testSource; - DynamicContainerTestDescriptor(UniqueId uniqueId, DynamicContainer dynamicContainer, TestSource testSource) { + DynamicContainerTestDescriptor(TestFactoryTestDescriptor root, UniqueId uniqueId, DynamicContainer dynamicContainer, + TestSource testSource) { super(uniqueId, dynamicContainer.getDisplayName()); + this.root = root; this.dynamicContainer = dynamicContainer; this.testSource = testSource; setSource(testSource); @@ -46,7 +50,11 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte DynamicTestExecutor dynamicTestExecutor) throws Exception { int index = 1; for (DynamicNode childNode : dynamicContainer.getDynamicNodes()) { - dynamicTestExecutor.execute(createDynamicDescriptor(this, childNode, index++, testSource)); + JupiterTestDescriptor childDescriptor = root.createDynamicDescriptor(this, childNode, index++, testSource); + Optional optionalResult = dynamicTestExecutor.execute(childDescriptor); + if (root.breaking(childNode, optionalResult)) { + break; + } } return context; } 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 afb5280c1763..66c791eff9ae 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,11 +13,15 @@ import static org.junit.platform.commons.meta.API.Usage.Internal; import java.lang.reflect.Method; +import java.time.Instant; import java.util.Iterator; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicRuntime; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.extension.TestExtensionContext; import org.junit.jupiter.engine.execution.ExecutableInvoker; @@ -26,12 +30,14 @@ 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.commons.util.Preconditions; +import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; /** - * {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for {@link org.junit.jupiter.api.TestFactory @TestFactory} - * methods. + * {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for + * {@link org.junit.jupiter.api.TestFactory @TestFactory} methods. * * @since 5.0 */ @@ -43,8 +49,13 @@ public class TestFactoryTestDescriptor extends MethodTestDescriptor { private static final ExecutableInvoker executableInvoker = new ExecutableInvoker(); + private final AtomicBoolean broken; + private final Instant start; + public TestFactoryTestDescriptor(UniqueId uniqueId, Class testClass, Method testMethod) { super(uniqueId, testClass, testMethod); + this.broken = new AtomicBoolean(false); + this.start = Instant.now(); } // --- TestDescriptor ------------------------------------------------------ @@ -75,8 +86,14 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe Iterator iterator = dynamicNodeStream.iterator(); while (iterator.hasNext()) { DynamicNode dynamicNode = iterator.next(); + int currentIndex = index; + Preconditions.notNull(dynamicNode, () -> "dynamic node #" + currentIndex + + " must not be null. [testMethod=" + getTestMethod() + "]"); JupiterTestDescriptor descriptor = createDynamicDescriptor(this, dynamicNode, index++, source); - dynamicTestExecutor.execute(descriptor); + Optional optionalResult = dynamicTestExecutor.execute(descriptor); + if (breaking(dynamicNode, optionalResult)) { + break; + } } } catch (ClassCastException ex) { @@ -95,7 +112,32 @@ private Stream toDynamicNodeStream(Object testFactoryMethodResult) } } - static JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, int index, + boolean breaking(DynamicNode dynamicNode, Optional testExecutionResult) { + // already broken? stay broken. + if (broken.get()) { + return true; + } + // alive, let node decide what to do... + class Info implements DynamicRuntime { + @Override + public Instant getInstantOfTestFactoryStart() { + return start; + } + + @Override + public boolean wasLastExecutableSuccessful() { + return testExecutionResult.map(TestExecutionResult::isSuccessful).orElse(false); + } + } + if (dynamicNode.breaking(new Info())) { + broken.set(true); + return true; + } + // alive, and still here? stay alive. + return false; + } + + JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, int index, TestSource source) { JupiterTestDescriptor descriptor; if (node instanceof DynamicTest) { @@ -106,7 +148,7 @@ static JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor paren else { DynamicContainer container = (DynamicContainer) node; UniqueId uniqueId = parent.getUniqueId().append(DYNAMIC_CONTAINER_SEGMENT_TYPE, "#" + index); - descriptor = new DynamicContainerTestDescriptor(uniqueId, container, source); + descriptor = new DynamicContainerTestDescriptor(this, uniqueId, container, source); } parent.addChild(descriptor); return descriptor; diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DynamicTestGenerationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DynamicTestGenerationTests.java index cb3d648b05d9..6f4ca597c4fa 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DynamicTestGenerationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DynamicTestGenerationTests.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.test.event.ExecutionEventConditions.assertRecordedExecutionEventsContainsExactly; @@ -32,11 +33,15 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.Executable; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.test.event.ExecutionEventRecorder; @@ -53,7 +58,7 @@ class DynamicTestGenerationTests extends AbstractJupiterTestEngineTests { void testFactoryMethodsAreCorrectlyDiscoveredForClassSelector() { LauncherDiscoveryRequest request = request().selectors(selectClass(MyDynamicTestCase.class)).build(); TestDescriptor engineDescriptor = discoverTests(request); - assertEquals(5, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); + assertEquals(6, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @Test @@ -86,20 +91,20 @@ void dynamicTestsAreExecutedFromStream() { event(engine(), finishedSuccessfully())); } + @Test + void dynamicTestsAreExecutedFromArray() { + LauncherDiscoveryRequest request = request().selectors( + DiscoverySelectors.selectMethod(MyDynamicTestCase.class, "dynamicArray")).build(); + + assertAllMyDynamicTestAreExecuted(executeTests(request)); + } + @Test void dynamicTestsAreExecutedFromCollection() { LauncherDiscoveryRequest request = request().selectors( DiscoverySelectors.selectMethod(MyDynamicTestCase.class, "dynamicCollection")).build(); - ExecutionEventRecorder eventRecorder = executeTests(request); - - assertAll( // - () -> assertEquals(3, eventRecorder.getContainerStartedCount(), "# container started"), - () -> assertEquals(2, eventRecorder.getDynamicTestRegisteredCount(), "# dynamic registered"), - () -> assertEquals(2, eventRecorder.getTestStartedCount(), "# tests started"), - () -> assertEquals(1, eventRecorder.getTestSuccessfulCount(), "# tests succeeded"), - () -> assertEquals(1, eventRecorder.getTestFailedCount(), "# tests failed"), - () -> assertEquals(3, eventRecorder.getContainerFinishedCount(), "# container finished")); + assertAllMyDynamicTestAreExecuted(executeTests(request)); } @Test @@ -107,15 +112,7 @@ void dynamicTestsAreExecutedFromIterator() { LauncherDiscoveryRequest request = request().selectors( DiscoverySelectors.selectMethod(MyDynamicTestCase.class, "dynamicIterator")).build(); - ExecutionEventRecorder eventRecorder = executeTests(request); - - assertAll( // - () -> assertEquals(3, eventRecorder.getContainerStartedCount(), "# container started"), - () -> assertEquals(2, eventRecorder.getDynamicTestRegisteredCount(), "# dynamic registered"), - () -> assertEquals(2, eventRecorder.getTestStartedCount(), "# tests started"), - () -> assertEquals(1, eventRecorder.getTestSuccessfulCount(), "# tests succeeded"), - () -> assertEquals(1, eventRecorder.getTestFailedCount(), "# tests failed"), - () -> assertEquals(3, eventRecorder.getContainerFinishedCount(), "# container finished")); + assertAllMyDynamicTestAreExecuted(executeTests(request)); } @Test @@ -123,16 +120,22 @@ void dynamicTestsAreExecutedFromIterable() { LauncherDiscoveryRequest request = request().selectors( DiscoverySelectors.selectMethod(MyDynamicTestCase.class, "dynamicIterable")).build(); - ExecutionEventRecorder eventRecorder = executeTests(request); + assertAllMyDynamicTestAreExecuted(executeTests(request)); + } + + private void assertAllMyDynamicTestAreExecuted(ExecutionEventRecorder eventRecorder) { + assertAllRecordedEventCounts(eventRecorder, 3, 2, 2, 1, 1, 3); + } + private void assertAllRecordedEventCounts(ExecutionEventRecorder eventRecorder, int... expected) { // @TestFactory methods are counted as both container and test assertAll( // - () -> assertEquals(3, eventRecorder.getContainerStartedCount(), "# container started"), - () -> assertEquals(2, eventRecorder.getDynamicTestRegisteredCount(), "# dynamic registered"), - () -> assertEquals(2, eventRecorder.getTestStartedCount(), "# tests started"), - () -> assertEquals(1, eventRecorder.getTestSuccessfulCount(), "# tests succeeded"), - () -> assertEquals(1, eventRecorder.getTestFailedCount(), "# tests failed"), - () -> assertEquals(3, eventRecorder.getContainerFinishedCount(), "# container finished")); + () -> assertEquals(expected[0], eventRecorder.getContainerStartedCount(), "# container started"), + () -> assertEquals(expected[1], eventRecorder.getDynamicTestRegisteredCount(), "# dynamic registered"), + () -> assertEquals(expected[2], eventRecorder.getTestStartedCount(), "# tests started"), + () -> assertEquals(expected[3], eventRecorder.getTestSuccessfulCount(), "# tests succeeded"), + () -> assertEquals(expected[4], eventRecorder.getTestFailedCount(), "# tests failed"), + () -> assertEquals(expected[5], eventRecorder.getContainerFinishedCount(), "# container finished")); } private static class MyDynamicTestCase { @@ -141,6 +144,11 @@ private static class MyDynamicTestCase { dynamicTest("succeedingTest", () -> assertTrue(true, "succeeding")), dynamicTest("failingTest", () -> fail("failing"))); + @TestFactory + DynamicTest[] dynamicArray() { + return list.toArray(new DynamicTest[0]); + } + @TestFactory Collection dynamicCollection() { return list; @@ -163,4 +171,63 @@ Iterable dynamicIterable() { } + @Test + void dynamicNodesWithRequiredTestsAreSuccessful() { + LauncherDiscoveryRequest request = request().selectors( + DiscoverySelectors.selectMethod(RequiredDynamicTestCase.class, "successful")).build(); + + assertAllRecordedEventCounts(executeTests(request), 4, 6, 6, 6, 0, 4); + } + + @Test + void dynamicNodesWithRequiredTestsFailingLogin() { + LauncherDiscoveryRequest request = request().selectors( + DiscoverySelectors.selectMethod(RequiredDynamicTestCase.class, "failLogin")).build(); + + assertAllRecordedEventCounts(executeTests(request), 3, 2, 2, 1, 1, 3); + } + + @Test + void dynamicNodesWithRequiredTestsFailingPage() { + LauncherDiscoveryRequest request = request().selectors( + DiscoverySelectors.selectMethod(RequiredDynamicTestCase.class, "failPage")).build(); + + assertAllRecordedEventCounts(executeTests(request), 4, 4, 4, 3, 1, 4); + } + + private static class RequiredDynamicTestCase { + + private final Executable empty = () -> { + }; + + private DynamicNode[] dynamicNodesWithRequiredTests(Optional failLogin, Optional failPage) { + return new DynamicNode[] { // + dynamicTest("Visit page requiring authorization while not logged in", empty), + dynamicTest("Log-in", () -> failLogin.ifPresent(Assertions::fail), i -> !failLogin.isPresent()), + dynamicContainer("Can access several pages while logged in", + dynamicTest("Visit second page", empty), + dynamicTest("Visit third page", () -> failPage.ifPresent(Assertions::fail), + i -> !failPage.isPresent()), + dynamicTest("Visit fourth page", empty)), + dynamicTest("Log-out", empty) // + }; + } + + @TestFactory + DynamicNode[] successful() { + return dynamicNodesWithRequiredTests(Optional.empty(), Optional.empty()); + } + + @TestFactory + DynamicNode[] failLogin() { + return dynamicNodesWithRequiredTests(Optional.of("you shall not pass"), Optional.empty()); + } + + @TestFactory + DynamicNode[] failPage() { + return dynamicNodesWithRequiredTests(Optional.empty(), Optional.of("page not rendered as expected")); + } + + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestExecutionResult.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestExecutionResult.java index 02956930c20e..34459449496c 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestExecutionResult.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestExecutionResult.java @@ -101,6 +101,14 @@ public Status getStatus() { return status; } + /** + * Returns {@code true} if the status of this result is + * {@linkplain Status#SUCCESSFUL Status.SUCCESSFUL}, else {@code false}. + */ + public boolean isSuccessful() { + return status == SUCCESSFUL; + } + /** * Get the throwable that caused this result, if available. */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java index 4d780c2c2f24..6cb644678beb 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java @@ -12,6 +12,8 @@ import static org.junit.platform.commons.util.BlacklistedExceptions.rethrowIfBlacklisted; +import java.util.Optional; + import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestDescriptor; @@ -51,7 +53,8 @@ void execute() { execute(this.rootTestDescriptor, this.rootContext, new ExecutionTracker()); } - private void execute(TestDescriptor testDescriptor, C parentContext, ExecutionTracker tracker) { + private Optional execute(TestDescriptor testDescriptor, C parentContext, + ExecutionTracker tracker) { Node node = asNode(testDescriptor); tracker.markExecuted(testDescriptor); @@ -61,7 +64,7 @@ private void execute(TestDescriptor testDescriptor, C parentContext, ExecutionTr SkipResult skipResult = node.shouldBeSkipped(preparedContext); if (skipResult.isSkipped()) { this.listener.executionSkipped(testDescriptor, skipResult.getReason().orElse("")); - return; + return Optional.empty(); } } catch (Throwable throwable) { @@ -69,7 +72,7 @@ private void execute(TestDescriptor testDescriptor, C parentContext, ExecutionTr // We call executionStarted first to comply with the contract of EngineExecutionListener this.listener.executionStarted(testDescriptor); this.listener.executionFinished(testDescriptor, TestExecutionResult.failed(throwable)); - return; + return Optional.empty(); } this.listener.executionStarted(testDescriptor); @@ -82,7 +85,7 @@ private void execute(TestDescriptor testDescriptor, C parentContext, ExecutionTr C contextForDynamicChildren = context; context = node.execute(context, dynamicTestDescriptor -> { this.listener.dynamicTestRegistered(dynamicTestDescriptor); - execute(dynamicTestDescriptor, contextForDynamicChildren, tracker); + return execute(dynamicTestDescriptor, contextForDynamicChildren, tracker); }); C contextForStaticChildren = context; @@ -98,6 +101,7 @@ private void execute(TestDescriptor testDescriptor, C parentContext, ExecutionTr }); this.listener.executionFinished(testDescriptor, result); + return Optional.of(result); } @SuppressWarnings("unchecked") diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java index 77e77be7c303..e6cc394e6de6 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java @@ -17,6 +17,7 @@ import org.junit.platform.commons.meta.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; /** * A node within the execution hierarchy. @@ -187,7 +188,7 @@ interface DynamicTestExecutor { * * @param testDescriptor the test descriptor to be executed */ - void execute(TestDescriptor testDescriptor); + Optional execute(TestDescriptor testDescriptor); }