Repository: micronaut-projects/micronaut-test
Pull request #784 (2023-06-09 15:15:57)

Add to the text context: test name and if the test method interceptor is supported
I tried to implement the test method interceptor for Kotest by failed because the provided interceptor in Kotlin is suspended


Patch:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 576efca6..38a5825e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
 [versions]
 micronaut-docs = "2.0.0"
-micronaut = "4.0.0-M6"
+micronaut = "4.0.0-M7"
 
 groovy = "4.0.11"
 
@@ -13,14 +13,15 @@ managed-rest-assured = "5.3.0"
 managed-kotest = "5.6.2"
 managed-spock = "2.3-groovy-4.0"
 
-kotlin = "1.8.21"
+kotlin = "1.8.22"
 graal-svm = "22.3.2"
 
-micronaut-data = "4.0.0-M6"
-micronaut-hibernate-validator = "4.0.0-M4"
-micronaut-serde = "2.0.0-M9"
-micronaut-spring = "5.0.0-M2"
-micronaut-sql = "5.0.0-M8"
+micronaut-data = "4.0.0-M7"
+micronaut-hibernate-validator = "4.0.0-M5"
+micronaut-serde = "2.0.0-M10"
+micronaut-spring = "5.0.0-M3"
+micronaut-sql = "5.0.0-M9"
+micronaut-reactor = "3.0.0-M6"
 
 micronaut-gradle-plugin = "4.0.0-M4"
 
@@ -33,6 +34,7 @@ micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronau
 micronaut-serde = { module = "io.micronaut.serde:micronaut-serde-bom", version.ref = "micronaut-serde" }
 micronaut-spring = { module = "io.micronaut.spring:micronaut-spring-bom", version.ref = "micronaut-spring" }
 micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" }
+micronaut-reactor = { module = 'io.micronaut.reactor:micronaut-reactor-bom', version.ref = "micronaut-reactor" }
 
 managed-assertj-core = { module = "org.assertj:assertj-core", version.ref = "managed-assertj" }
 managed-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "managed-hamcrest" }
diff --git a/settings.gradle b/settings.gradle
index 135cbac3..6ffa1fb1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -18,6 +18,7 @@ micronautBuild {
     importMicronautCatalog("micronaut-serde")
     importMicronautCatalog("micronaut-spring")
     importMicronautCatalog("micronaut-sql")
+    importMicronautCatalog("micronaut-reactor")
 }
 
 rootProject.name = 'test-parent'


Test patch:
diff --git a/test-core/src/main/java/io/micronaut/test/context/TestContext.java b/test-core/src/main/java/io/micronaut/test/context/TestContext.java
index 26a25f42..b4a45508 100644
--- a/test-core/src/main/java/io/micronaut/test/context/TestContext.java
+++ b/test-core/src/main/java/io/micronaut/test/context/TestContext.java
@@ -16,6 +16,8 @@
 package io.micronaut.test.context;
 
 import io.micronaut.context.ApplicationContext;
+import io.micronaut.core.annotation.NonNull;
+
 import java.lang.reflect.AnnotatedElement;
 
 /**
@@ -26,66 +28,88 @@ import java.lang.reflect.AnnotatedElement;
  */
 public class TestContext {
 
-  private final ApplicationContext applicationContext;
-  private final Class<?> testClass;
-  private final AnnotatedElement testMethod;
-  private final Throwable testException;
-  private final Object testInstance;
+    private final ApplicationContext applicationContext;
+    private final Class<?> testClass;
+    private final AnnotatedElement testMethod;
+    private final Throwable testException;
+    private final Object testInstance;
+    private final String testName;
+    private final boolean supportsTestMethodInterceptors;
+
+    /**
+     * @param applicationContext       The application context
+     * @param testClass                The test class
+     * @param testMethod               The test method
+     * @param testInstance             The test instance
+     * @param testException            The exception thrown by the test execution
+     * @param testName                 The test name
+     * @param supportsTestMethodInterceptors The indicator if the test framework supports {@link TestMethodInterceptor}.
+     */
+    public TestContext(
+        final ApplicationContext applicationContext,
+        final Class<?> testClass,
+        final AnnotatedElement testMethod,
+        final Object testInstance,
+        final Throwable testException,
+        final String testName,
+        boolean supportsTestMethodInterceptors) {
 
-  /**
-   * @param applicationContext The application context
-   * @param testClass The test class
-   * @param testMethod The test method
-   * @param testInstance The test instance
-   * @param testException The exception thrown by the test execution
-   */
-  public TestContext(
-      final ApplicationContext applicationContext,
-      final Class<?> testClass,
-      final AnnotatedElement testMethod,
-      final Object testInstance,
-      final Throwable testException) {
+        this.applicationContext = applicationContext;
+        this.testClass = testClass;
+        this.testException = testException;
+        this.testInstance = testInstance;
+        this.testMethod = testMethod;
+        this.testName = testName;
+        this.supportsTestMethodInterceptors = supportsTestMethodInterceptors;
+    }
 
-    this.applicationContext = applicationContext;
-    this.testClass = testClass;
-    this.testException = testException;
-    this.testInstance = testInstance;
-    this.testMethod = testMethod;
-  }
+    /**
+     * @return The application context
+     */
+    public ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
 
-  /**
-   * @return The application context
-   */
-  public ApplicationContext getApplicationContext() {
-    return applicationContext;
-  }
+    /**
+     * @return The test class
+     */
+    public Class<?> getTestClass() {
+        return testClass;
+    }
 
-  /**
-   * @return The test class
-   */
-  public Class<?> getTestClass() {
-    return testClass;
-  }
+    /**
+     * @return The test instance
+     */
+    public Throwable getTestException() {
+        return testException;
+    }
 
-  /**
-   * @return The test instance
-   */
-  public Throwable getTestException() {
-    return testException;
-  }
+    /**
+     * @return The test method
+     */
+    public AnnotatedElement getTestMethod() {
+        return testMethod;
+    }
 
-  /**
-   * @return The test method
-   */
-  public AnnotatedElement getTestMethod() {
-    return testMethod;
-  }
+    /**
+     * @return The exception thrown by the test execution
+     */
+    public Object getTestInstance() {
+        return testInstance;
+    }
 
-  /**
-   * @return The exception thrown by the test execution
-   */
-  public Object getTestInstance() {
-    return testInstance;
-  }
+    /**
+     * @return The test name
+     */
+    @NonNull
+    public String getTestName() {
+        return testName;
+    }
 
+    /**
+     * @return True if {@link TestMethodInterceptor} is supported
+     */
+    public boolean isSupportsTestMethodInterceptors() {
+        return supportsTestMethodInterceptors;
+    }
 }
diff --git a/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java b/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java
index 853e22da..47b0c425 100644
--- a/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java
+++ b/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java
@@ -90,6 +90,13 @@ public abstract class AbstractMicronautExtension<C> implements TestExecutionList
     private List<TestExecutionListener> listeners;
     private List<TestMethodInterceptor<Object>> interceptors;
 
+    /**
+     * @return True if there are interceptors
+     */
+    protected boolean hasInterceptors() {
+        return !interceptors.isEmpty();
+    }
+
     @Override
     public Object interceptBeforeEach(TestMethodInvocationContext<Object> methodInvocationContext) throws Throwable {
         return interceptBeforeEach(methodInvocationContext, interceptors);
@@ -162,9 +169,6 @@ public abstract class AbstractMicronautExtension<C> implements TestExecutionList
         });
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void beforeTestExecution(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::beforeTestExecution, testContext, false);
@@ -180,25 +184,16 @@ public abstract class AbstractMicronautExtension<C> implements TestExecutionList
         fireListeners(TestExecutionListener::afterCleanupTest, testContext, true);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void afterTestExecution(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::afterTestExecution, testContext, true);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void beforeTestClass(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::beforeTestClass, testContext, false);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void afterTestClass(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::afterTestClass, testContext, true);
@@ -214,17 +209,11 @@ public abstract class AbstractMicronautExtension<C> implements TestExecutionList
         fireListeners(TestExecutionListener::afterSetupTest, testContext, true);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void beforeTestMethod(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::beforeTestMethod, testContext, false);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void afterTestMethod(TestContext testContext) throws Exception {
         fireListeners(TestExecutionListener::afterTestMethod, testContext, true);
diff --git a/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java b/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java
index 17ffe866..8793474a 100644
--- a/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java
+++ b/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java
@@ -330,7 +330,9 @@ public class MicronautJunit5Extension extends AbstractMicronautExtension<Extensi
           context.getTestClass().orElse(null),
           context.getTestMethod().orElse(null),
           context.getTestInstance().orElse(null),
-          context.getExecutionException().orElse(null));
+          context.getExecutionException().orElse(null),
+          context.getDisplayName(),
+          true);
     }
 
     @Override
diff --git a/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt b/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt
index 45a401a3..15ce5a77 100644
--- a/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt
+++ b/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt
@@ -60,7 +60,6 @@ class MicronautKotest5Context(
         afterClass(spec)
     }
 
-    @Suppress("UNCHECKED_CAST")
     fun beforeTest(testCase: TestCase) {
         val filter = testCase.spec::class.memberFunctions.filter { it.name == testCase.name.testName }
         var propertyAnnotations: List<Property>? = emptyList()
@@ -86,7 +85,15 @@ class MicronautKotest5Context(
     fun getSpecDefinition() = specDefinition
 
     fun buildContext(spec: Spec): TestContext {
-        return TestContext(applicationContext, spec.javaClass, null, spec, null)
+        return TestContext(
+            applicationContext,
+            spec.javaClass,
+            null,
+            spec,
+            null,
+            spec.javaClass.simpleName,
+            false
+        )
     }
 
     fun buildContext(testCase: TestCase, result: TestResult?): TestContext {
@@ -98,7 +105,15 @@ class MicronautKotest5Context(
             null -> null
         }
 
-        return TestContext(applicationContext, testCase.spec.javaClass, testCase.test.javaClass, testCase.spec, error)
+        return TestContext(
+            applicationContext,
+            testCase.spec.javaClass,
+            testCase.test.javaClass,
+            testCase.spec,
+            error,
+            testCase.name.testName,
+            false
+        )
     }
 
 }
diff --git a/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/InterceptTest.kt b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/InterceptTest.kt
new file mode 100644
index 00000000..bef9e5ef
--- /dev/null
+++ b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/InterceptTest.kt
@@ -0,0 +1,45 @@
+package io.micronaut.test.kotest5.intercept
+
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.shouldBe
+import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
+import io.micronaut.test.kotest5.MathService
+
+@MicronautTest
+class InterceptTest(
+    private val mathService: MathService,
+    private val interceptor: TestInterceptor
+) : BehaviorSpec({
+
+    given("the math service") {
+
+        `when`("the service is called with 2") {
+            val result = mathService.compute(2) // <3>
+            then("the result is 8") {
+                result shouldBe 8
+            }
+        }
+
+        `when`("validate invocations") {
+            interceptor.calls shouldBe listOf(
+                "BEFORE TEST CLASS InterceptTest",
+                "BEFORE TEST EXECUTION the math service",
+                "BEFORE TEST METHOD the math service",
+                "BEFORE TEST EXECUTION the math service",
+                "BEFORE TEST EXECUTION the service is called with 2",
+                "BEFORE TEST METHOD the service is called with 2",
+                "BEFORE TEST EXECUTION the service is called with 2",
+                "BEFORE TEST METHOD the result is 8",
+                "BEFORE TEST EXECUTION the result is 8",
+                "AFTER TEST EXECUTION the result is 8",
+                "AFTER TEST METHOD the result is 8",
+                "AFTER TEST EXECUTION the service is called with 2",
+                "AFTER TEST METHOD the service is called with 2",
+                "AFTER TEST EXECUTION the service is called with 2",
+                "BEFORE TEST EXECUTION validate invocations",
+                "BEFORE TEST METHOD validate invocations",
+                "BEFORE TEST EXECUTION validate invocations"
+            )
+        }
+    }
+})
diff --git a/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/TestInterceptor.kt b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/TestInterceptor.kt
new file mode 100644
index 00000000..7619be02
--- /dev/null
+++ b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/intercept/TestInterceptor.kt
@@ -0,0 +1,100 @@
+package io.micronaut.test.kotest5.intercept
+
+import io.micronaut.context.annotation.Property
+import io.micronaut.test.context.TestContext
+import io.micronaut.test.context.TestExecutionListener
+import io.micronaut.test.context.TestMethodInterceptor
+import io.micronaut.test.context.TestMethodInvocationContext
+import jakarta.inject.Singleton
+
+@Singleton
+@Property(name = "InterceptTestSpec", value = "true")
+class TestInterceptor : TestMethodInterceptor<Any>, TestExecutionListener {
+    var calls: MutableList<String> = ArrayList()
+
+    @Throws(Exception::class)
+    override fun beforeTestClass(testContext: TestContext) {
+        calls.add("BEFORE TEST CLASS " + testContext.testName)
+        if (testContext.isSupportsTestMethodInterceptors) {
+            throw IllegalStateException("Not supported")
+        }
+    }
+
+    @Throws(Exception::class)
+    override fun beforeSetupTest(testContext: TestContext) {
+        calls.add("BEFORE SETUP TEST " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun afterSetupTest(testContext: TestContext) {
+        calls.add("AFTER SETUP TEST " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun beforeCleanupTest(testContext: TestContext) {
+        calls.add("BEFORE CLEANUP TEST " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun afterCleanupTest(testContext: TestContext) {
+        calls.add("AFTER CLEANUP TEST " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun beforeTestMethod(testContext: TestContext) {
+        calls.add("BEFORE TEST METHOD " + testContext.testName)
+        if (testContext.isSupportsTestMethodInterceptors) {
+            throw IllegalStateException("Not supported")
+        }
+    }
+
+    @Throws(Exception::class)
+    override fun beforeTestExecution(testContext: TestContext) {
+        calls.add("BEFORE TEST EXECUTION " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun afterTestExecution(testContext: TestContext) {
+        calls.add("AFTER TEST EXECUTION " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun afterTestMethod(testContext: TestContext) {
+        calls.add("AFTER TEST METHOD " + testContext.testName)
+    }
+
+    @Throws(Exception::class)
+    override fun afterTestClass(testContext: TestContext) {
+        calls.add("AFTER TEST CLASS " + testContext.testName)
+    }
+
+    @Throws(Throwable::class)
+    override fun interceptBeforeEach(methodInvocationContext: TestMethodInvocationContext<Any>): Any {
+        calls.add("IN BEFORE " + methodInvocationContext.testContext.testName)
+        return try {
+            methodInvocationContext.proceed()
+        } finally {
+            calls.add("OUT BEFORE " + methodInvocationContext.testContext.testName)
+        }
+    }
+
+    @Throws(Throwable::class)
+    override fun interceptTest(methodInvocationContext: TestMethodInvocationContext<Any>): Any {
+        calls.add("IN " + methodInvocationContext.testContext.testName)
+        return try {
+            methodInvocationContext.proceed()
+        } finally {
+            calls.add("OUT " + methodInvocationContext.testContext.testName)
+        }
+    }
+
+    @Throws(Throwable::class)
+    override fun interceptAfterEach(methodInvocationContext: TestMethodInvocationContext<Any>): Any {
+        calls.add("IN AFTER " + methodInvocationContext.testContext.testName)
+        return try {
+            methodInvocationContext.proceed()
+        } finally {
+            calls.add("OUT AFTER " + methodInvocationContext.testContext.testName)
+        }
+    }
+}
diff --git a/test-spock/build.gradle b/test-spock/build.gradle
index c3089e3d..46b73e41 100644
--- a/test-spock/build.gradle
+++ b/test-spock/build.gradle
@@ -22,6 +22,7 @@ dependencies {
 //    testImplementation(mn.micronaut.hibernate.gorm)
     testImplementation(mnSpring.micronaut.spring)
     testImplementation(mn.snakeyaml)
+    testImplementation(mnReactor.micronaut.reactor)
 
     testRuntimeOnly(mnSerde.micronaut.serde.jackson)
     testRuntimeOnly(mnSql.h2)
diff --git a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
index c8915ff6..96803aa0 100644
--- a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
+++ b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
@@ -218,12 +218,27 @@ public class MicronautSpockExtension<T extends Annotation> extends AbstractMicro
     }
 
     private TestContext buildContext(IMethodInvocation invocation, Throwable exception) {
+        String displayName;
+        if (invocation.getFeature() == null) {
+            if (invocation.getMethod() != null) {
+                displayName = invocation.getMethod().getName();
+                if (displayName == null) {
+                    displayName = invocation.getMethod().getKind().name();
+                }
+            } else {
+                displayName = "";
+            }
+        } else {
+            displayName = invocation.getFeature().getDisplayName();
+        }
         return new TestContext(
             applicationContext,
             Optional.ofNullable(invocation.getSpec()).map(SpecInfo::getReflection).orElse(null),
             Optional.ofNullable(invocation.getFeature()).map(FeatureInfo::getFeatureMethod).map(MethodInfo::getReflection).orElse(null),
             invocation.getInstance(),
-            exception);
+            exception,
+            displayName,
+            true);
     }
 
     @Override