From f69d9f5d6b13d31602de44463e40877f27f7e781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Fri, 21 Aug 2026 13:45:43 +0000 Subject: [PATCH] Add TestMethodReturnValueHandler SPI for non-void @Test methods Introduce a new extension point that allows frameworks to support @Test methods with non-void return types. This enables reactive frameworks (e.g., Quarkus with Mutiny Uni) to let test methods return their native types instead of requiring bytecode transformation to void. The SPI is discovered via ServiceLoader. During discovery, IsTestableMethod accepts methods with a registered handler. During execution, the handler receives an Invocation it must proceed() on, giving it control over both the execution context and return value processing. Co-Authored-By: Claude Opus 4.6 --- documentation/antora.yml | 1 + documentation/modules/ROOT/nav.adoc | 1 + .../test-method-return-value-handling.adoc | 71 ++++++++++++ .../release-notes/release-notes-6.2.0-M1.adoc | 5 + .../TestMethodReturnValueHandler.java | 95 +++++++++++++++ .../src/main/java/module-info.java | 1 + .../descriptor/TestMethodTestDescriptor.java | 43 ++++++- .../predicates/IsTestableMethod.java | 6 +- .../engine/support/MethodReflectionUtils.java | 26 +++++ .../CompletableFutureReturnValueHandler.java | 46 ++++++++ .../TestMethodReturnValueHandlerTests.java | 109 ++++++++++++++++++ ...api.extension.TestMethodReturnValueHandler | 1 + 12 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 documentation/modules/ROOT/pages/extensions/test-method-return-value-handling.adoc create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/CompletableFutureReturnValueHandler.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/TestMethodReturnValueHandlerTests.java create mode 100644 jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler diff --git a/documentation/antora.yml b/documentation/antora.yml index d809a8ee47a5..39bb8cf9fe1c 100644 --- a/documentation/antora.yml +++ b/documentation/antora.yml @@ -212,6 +212,7 @@ asciidoc: TestInstancePreDestroyCallback: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestInstancePreDestroyCallback.html[TestInstancePreDestroyCallback]' TestTemplateInvocationContext: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContext.html[TestTemplateInvocationContext]' TestTemplateInvocationContextProvider: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.html[TestTemplateInvocationContextProvider]' + TestMethodReturnValueHandler: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.html[TestMethodReturnValueHandler]' TestWatcher: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestWatcher.html[TestWatcher]' PreInterruptCallback: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/PreInterruptCallback.html[PreInterruptCallback]' # Jupiter Conditions diff --git a/documentation/modules/ROOT/nav.adoc b/documentation/modules/ROOT/nav.adoc index 568e8e4dc25a..6fb0baf1dd42 100644 --- a/documentation/modules/ROOT/nav.adoc +++ b/documentation/modules/ROOT/nav.adoc @@ -49,6 +49,7 @@ ** xref:extensions/exception-handling.adoc[] ** xref:extensions/pre-interrupt-callback.adoc[] ** xref:extensions/intercepting-invocations.adoc[] +** xref:extensions/test-method-return-value-handling.adoc[] ** xref:extensions/providing-invocation-contexts-for-class-templates.adoc[] ** xref:extensions/providing-invocation-contexts-for-test-templates.adoc[] ** xref:extensions/keeping-state-in-extensions.adoc[] diff --git a/documentation/modules/ROOT/pages/extensions/test-method-return-value-handling.adoc b/documentation/modules/ROOT/pages/extensions/test-method-return-value-handling.adoc new file mode 100644 index 000000000000..2e7ef2805ced --- /dev/null +++ b/documentation/modules/ROOT/pages/extensions/test-method-return-value-handling.adoc @@ -0,0 +1,71 @@ += Test Method Return Value Handling + +`{TestMethodReturnValueHandler}` defines an extension point that allows `@Test` methods +to return non-void types. By default, JUnit Jupiter requires `@Test` methods to return +`void`. A registered `TestMethodReturnValueHandler` relaxes this requirement for return +types it supports. + +[[registration]] +== Registration + +Implementations are discovered via Java's `{ServiceLoader}` mechanism and must be +registered in a file named +`META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler` on the +classpath. + +[[how-it-works]] +== How It Works + +A `TestMethodReturnValueHandler` has two responsibilities: + +1. **Declare supported return types** via `supportsReturnType(Class)` — during + discovery, JUnit calls this method to decide whether a non-void `@Test` method should + be accepted. + +2. **Execute the test method** via `execute(Invocation, ExtensionContext)` — at + execution time, the handler receives an `{InvocationInterceptor}.Invocation` that wraps + the test method call. The handler must call `invocation.proceed()` to invoke the test + and obtain the return value. It is then responsible for processing the result — for + example, subscribing to a reactive type and awaiting completion. + +Because the handler controls _when_ `proceed()` is called, it can set up a custom +execution context beforehand — for example, running the test on a specific thread or +executor. + +[[example]] +== Example + +The following handler adds support for `@Test` methods that return `CompletableFuture`: + +[source,java,indent=0] +---- +public class CompletableFutureHandler implements TestMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(Class returnType) { + return CompletableFuture.class.isAssignableFrom(returnType); + } + + @Override + public void execute(InvocationInterceptor.Invocation invocation, + ExtensionContext context) throws Throwable { + Object result = invocation.proceed(); + if (result != null) { + ((CompletableFuture) result).get(30, TimeUnit.SECONDS); + } + } +} +---- + +With this handler registered, the following test is discovered and executed: + +[source,java,indent=0] +---- +@Test +CompletableFuture asyncTest() { + return CompletableFuture.supplyAsync(() -> { + // test logic + return "result"; + }); +} +---- diff --git a/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc b/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc index 2ca0b5e52cde..7211c9516c0c 100644 --- a/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc +++ b/documentation/modules/ROOT/partials/release-notes/release-notes-6.2.0-M1.adoc @@ -71,6 +71,11 @@ repository on GitHub. and `@DisabledOnOs`. * Failures caused by `@Timeout` expirations now include a hint about enabling xref:writing-tests/timeouts.adoc#debugging-thread-dump[thread dumps]. +* Added `TestMethodReturnValueHandler` extension point that allows `@Test` methods to + return non-void types. Handlers are discovered via Java's `ServiceLoader` mechanism and + can control test method invocation — for example, to execute the test on a specific + thread or executor and then process the return value (e.g. awaiting a reactive type). + See xref:extensions/test-method-return-value-handling.adoc[] for details. [[v6.2.0-M1-junit-vintage]] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.java new file mode 100644 index 000000000000..8579b34ad556 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015-2026 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 + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * {@code TestMethodReturnValueHandler} allows extensions to support + * {@link org.junit.jupiter.api.Test @Test} methods that return a non-void + * value. + * + *

By default, JUnit Jupiter requires {@code @Test} methods to return + * {@code void}. A registered {@code TestMethodReturnValueHandler} relaxes + * this requirement for return types it {@linkplain #supportsReturnType + * supports}. The handler is responsible for invoking the test method + * (via {@link InvocationInterceptor.Invocation#proceed() + * invocation.proceed()}) and processing the return value. + * + *

Because the handler controls the invocation, it can set up a + * custom execution context before calling {@code proceed()} — for + * example, running the test method on a specific thread or executor. + * + *

Implementations are discovered via Java's {@link java.util.ServiceLoader} + * mechanism and must be registered in + * {@code META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler}. + * + *

Example

+ *
{@code
+ * public class CompletableFutureHandler implements TestMethodReturnValueHandler {
+ *
+ *     @Override
+ *     public boolean supportsReturnType(Class returnType) {
+ *         return CompletableFuture.class.isAssignableFrom(returnType);
+ *     }
+ *
+ *     @Override
+ *     public void execute(InvocationInterceptor.Invocation invocation,
+ *             ExtensionContext context) throws Throwable {
+ *         Object result = invocation.proceed();
+ *         if (result != null) {
+ *             ((CompletableFuture) result).get(30, TimeUnit.SECONDS);
+ *         }
+ *     }
+ * }
+ * }
+ *
+ * @since 6.2
+ * @see org.junit.jupiter.api.Test
+ */
+@API(status = EXPERIMENTAL, since = "6.2")
+public interface TestMethodReturnValueHandler extends Extension {
+
+	/**
+	 * Determine if this handler supports the supplied return type.
+	 *
+	 * @param returnType the return type of the test method; never {@code null}
+	 * @return {@code true} if this handler can handle the return type
+	 */
+	boolean supportsReturnType(Class returnType);
+
+	/**
+	 * Execute a {@code @Test} method that returns a supported type.
+	 *
+	 * 

The handler must call {@link InvocationInterceptor.Invocation#proceed() + * invocation.proceed()} to invoke the test method and obtain the return + * value. The handler is then responsible for processing the result + * — for example, subscribing to a reactive type and awaiting + * completion. + * + *

Because the handler controls when {@code proceed()} is called, it + * can set up a custom execution context beforehand — for example, + * running the test on a specific thread or executor. + * + *

If this method throws, the test is marked as failed with the thrown + * exception. + * + * @param invocation the invocation that executes the test method; + * calling {@code proceed()} invokes the method and returns its result; + * never {@code null} + * @param context the current extension context; never {@code null} + * @throws Throwable if the return value indicates a test failure + */ + void execute(InvocationInterceptor.Invocation invocation, ExtensionContext context) throws Throwable; + +} diff --git a/junit-jupiter-engine/src/main/java/module-info.java b/junit-jupiter-engine/src/main/java/module-info.java index 4db62b007f6a..ae9fdc7eee56 100644 --- a/junit-jupiter-engine/src/main/java/module-info.java +++ b/junit-jupiter-engine/src/main/java/module-info.java @@ -28,6 +28,7 @@ requires org.opentest4j; uses org.junit.jupiter.api.extension.Extension; + uses org.junit.jupiter.api.extension.TestMethodReturnValueHandler; provides org.junit.platform.engine.TestEngine with org.junit.jupiter.engine.JupiterTestEngine; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 99aa0ca67c9a..3338a4bf7de6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -23,6 +23,7 @@ import java.util.function.UnaryOperator; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; @@ -30,7 +31,9 @@ import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; +import org.junit.jupiter.api.extension.TestMethodReturnValueHandler; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback; import org.junit.jupiter.api.extension.TestInstances; @@ -39,10 +42,12 @@ import org.junit.jupiter.engine.execution.AfterEachMethodAdapter; import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter; import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker; +import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall; import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall.VoidMethodInterceptorCall; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; +import org.junit.jupiter.engine.support.MethodReflectionUtils; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; @@ -216,8 +221,15 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe try { Method testMethod = getTestMethod(); Object instance = extensionContext.getRequiredTestInstance(); - executableInvoker.invokeVoid(testMethod, instance, extensionContext, context.getExtensionRegistry(), - interceptorCall); + TestMethodReturnValueHandler handler = MethodReflectionUtils.findReturnValueHandler(testMethod); + if (handler != null) { + handler.execute(invokeTestMethodCapturingResult(testMethod, instance, + extensionContext, context.getExtensionRegistry()), extensionContext); + } + else { + executableInvoker.invokeVoid(testMethod, instance, extensionContext, + context.getExtensionRegistry(), interceptorCall); + } } catch (Throwable throwable) { UnrecoverableExceptions.rethrowIfUnrecoverable(throwable); @@ -226,6 +238,33 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe }); } + @SuppressWarnings("NullAway") + private InvocationInterceptor.Invocation invokeTestMethodCapturingResult(Method testMethod, Object instance, + ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { + return () -> executableInvoker.<@Nullable Object> invoke(testMethod, instance, + extensionContext, extensionRegistry, returnValueCapturingInterceptorCall()); + } + + @SuppressWarnings("NullAway") + private static ReflectiveInterceptorCall returnValueCapturingInterceptorCall() { + return (interceptor, invocation, invocationContext, extensionContext) -> { + Object[] resultHolder = new Object[1]; + interceptor.interceptTestMethod(new InvocationInterceptor.Invocation<@Nullable Void>() { + @Override + public @Nullable Void proceed() throws Throwable { + resultHolder[0] = invocation.proceed(); + return null; + } + + @Override + public void skip() { + invocation.skip(); + } + }, invocationContext, extensionContext); + return resultHolder[0]; + }; + } + private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context, Throwable throwable) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java index 1c5057cb0a7b..99d7f4520f2b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java @@ -14,6 +14,8 @@ import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; import static org.junit.platform.commons.support.ModifierSupport.isNotAbstract; +import org.junit.jupiter.engine.support.MethodReflectionUtils; + import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.function.BiFunction; @@ -65,7 +67,9 @@ private static Condition isNotPrivate(Class annota protected static Condition hasVoidReturnType(Class annotationType, DiscoveryIssueReporter issueReporter) { - return issueReporter.createReportingCondition(method -> getReturnType(method) == void.class, + return issueReporter.createReportingCondition( + method -> getReturnType(method) == void.class + || MethodReflectionUtils.hasReturnValueHandler(method), method -> createIssue(annotationType, method, "must not return a value")); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java index 783684cb145c..d5c92846fdd9 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java @@ -21,15 +21,41 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.TestMethodReturnValueHandler; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.KotlinReflectionUtils; @API(status = INTERNAL, since = "6.0") public class MethodReflectionUtils { + private static List getReturnValueHandlers() { + return ServiceLoader // + .load(TestMethodReturnValueHandler.class, ClassLoaderUtils.getDefaultClassLoader()) // + .stream() // + .map(ServiceLoader.Provider::get) // + .toList(); + } + + public static boolean hasReturnValueHandler(Method method) { + Class returnType = method.getReturnType(); + return returnType != void.class + && getReturnValueHandlers().stream().anyMatch(h -> h.supportsReturnType(returnType)); + } + + public static @Nullable TestMethodReturnValueHandler findReturnValueHandler(Method method) { + Class returnType = method.getReturnType(); + return getReturnValueHandlers().stream() // + .filter(h -> h.supportsReturnType(returnType)) // + .findFirst() // + .orElse(null); + } + public static Class getReturnType(Method method) { return isKotlinSuspendingFunction(method) // ? getKotlinSuspendingFunctionReturnType(method) // diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/CompletableFutureReturnValueHandler.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/CompletableFutureReturnValueHandler.java new file mode 100644 index 000000000000..f90fd93f9160 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/CompletableFutureReturnValueHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2026 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 + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.TestMethodReturnValueHandler; + +public class CompletableFutureReturnValueHandler implements TestMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(Class returnType) { + return CompletableFuture.class.isAssignableFrom(returnType); + } + + @Override + public void execute(InvocationInterceptor.Invocation invocation, ExtensionContext context) throws Throwable { + Object result = invocation.proceed(); + if (result == null) { + return; + } + try { + ((CompletableFuture) result).get(30, TimeUnit.SECONDS); + } + catch (ExecutionException ex) { + throw ex.getCause(); + } + catch (TimeoutException ex) { + throw new AssertionError("CompletableFuture did not complete within 30 seconds", ex); + } + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestMethodReturnValueHandlerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestMethodReturnValueHandlerTests.java new file mode 100644 index 000000000000..31cc335463c5 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestMethodReturnValueHandlerTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2026 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 + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine; + +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +/** + * Integration tests for {@link org.junit.jupiter.api.extension.TestMethodReturnValueHandler}. + * + * @since 6.2 + */ +class TestMethodReturnValueHandlerTests extends AbstractJupiterTestEngineTests { + + @Test + void testMethodReturningCompletableFutureIsDiscoveredAndExecuted() { + var results = executeTestsForClass(CompletableFutureTestCase.class); + + results.testEvents().assertStatistics( + stats -> stats.started(3).succeeded(2).failed(1)); + } + + @Test + void successfulCompletableFutureTestSucceeds() { + var results = executeTestsForClass(CompletableFutureTestCase.class); + + results.testEvents().succeeded().assertEventsMatchLoosely( + event(test("successfulAsyncTest"), finishedSuccessfully()), + event(test("voidTestStillWorks"), finishedSuccessfully())); + } + + @Test + void failedCompletableFutureTestFails() { + var results = executeTestsForClass(CompletableFutureTestCase.class); + + results.testEvents().failed().assertEventsMatchExactly( + event(test("failingAsyncTest"), finishedWithFailure( + instanceOf(RuntimeException.class), message("async failure")))); + } + + @Test + void nullReturnValueIsHandledGracefully() { + var results = executeTestsForClass(NullReturnTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(1).succeeded(1)); + results.testEvents().succeeded().assertEventsMatchExactly( + event(test("returnsNull"), finishedSuccessfully())); + } + + @Test + void unsupportedReturnTypeIsNotDiscovered() { + var results = executeTestsForClass(UnsupportedReturnTypeTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(0)); + } + + // ------------------------------------------------------------------- + + static class CompletableFutureTestCase { + + @Test + CompletableFuture successfulAsyncTest() { + return CompletableFuture.completedFuture("hello"); + } + + @Test + CompletableFuture failingAsyncTest() { + return CompletableFuture.failedFuture(new RuntimeException("async failure")); + } + + @Test + void voidTestStillWorks() { + } + } + + static class NullReturnTestCase { + + @SuppressWarnings("NullAway") + @Test + CompletableFuture returnsNull() { + return null; + } + } + + static class UnsupportedReturnTypeTestCase { + + @Test + String unsupportedReturnType() { + return "not supported"; + } + } + +} diff --git a/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler b/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler new file mode 100644 index 000000000000..ee89d41ff631 --- /dev/null +++ b/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler @@ -0,0 +1 @@ +org.junit.jupiter.engine.CompletableFutureReturnValueHandler