From 663e0401c1538fdd6d3d9128e0425aec56dc507d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 23 Aug 2021 09:32:15 +0300 Subject: [PATCH] Fix @InjectMock and @InjectSpy handling of @Nested tests Fixes: #19391 --- .../io/quarkus/it/mockbean/NestedTest.java | 49 ++++++++++++++++ .../ResetOuterMockitoMocksCallback.java | 14 +++++ .../SetMockitoMockAsBeanMockCallback.java | 3 + ...junit.callback.QuarkusTestAfterAllCallback | 1 + .../test/junit/QuarkusTestExtension.java | 56 +++++++++++++++++-- .../callback/QuarkusTestAfterAllCallback.java | 11 ++++ .../junit/callback/QuarkusTestContext.java | 23 ++++++++ .../callback/QuarkusTestMethodContext.java | 11 +--- 8 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/NestedTest.java create mode 100644 test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/ResetOuterMockitoMocksCallback.java create mode 100644 test-framework/junit5-mockito/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestAfterAllCallback.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java diff --git a/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/NestedTest.java b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/NestedTest.java new file mode 100644 index 0000000000000..94f4060f26521 --- /dev/null +++ b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/NestedTest.java @@ -0,0 +1,49 @@ +package io.quarkus.it.mockbean; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusTest +public class NestedTest { + + @InjectMock + MessageService messageService; + + @Nested + public class ActualTest { + + @InjectMock + SuffixService suffixService; + + @Test + public void testGreet() { + Mockito.when(messageService.getMessage()).thenReturn("hi"); + Mockito.when(suffixService.getSuffix()).thenReturn("!"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("HI!")); + } + + @Test + public void testGreetAgain() { + Mockito.when(messageService.getMessage()).thenReturn("yolo"); + Mockito.when(suffixService.getSuffix()).thenReturn("!!!"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("YOLO!!!")); + } + } +} diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/ResetOuterMockitoMocksCallback.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/ResetOuterMockitoMocksCallback.java new file mode 100644 index 0000000000000..ffbf7c0b35d82 --- /dev/null +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/ResetOuterMockitoMocksCallback.java @@ -0,0 +1,14 @@ +package io.quarkus.test.junit.mockito.internal; + +import io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback; +import io.quarkus.test.junit.callback.QuarkusTestContext; + +public class ResetOuterMockitoMocksCallback implements QuarkusTestAfterAllCallback { + + @Override + public void afterAll(QuarkusTestContext context) { + if (context.getOuterInstance() != null) { + MockitoMocksTracker.reset(context.getOuterInstance()); + } + } +} diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SetMockitoMockAsBeanMockCallback.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SetMockitoMockAsBeanMockCallback.java index 354a0d28378cc..3bbced045c6cb 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SetMockitoMockAsBeanMockCallback.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SetMockitoMockAsBeanMockCallback.java @@ -9,6 +9,9 @@ public class SetMockitoMockAsBeanMockCallback implements QuarkusTestBeforeEachCa @Override public void beforeEach(QuarkusTestMethodContext context) { MockitoMocksTracker.getMocks(context.getTestInstance()).forEach(this::installMock); + if (context.getOuterInstance() != null) { + MockitoMocksTracker.getMocks(context.getOuterInstance()).forEach(this::installMock); + } } private void installMock(MockitoMocksTracker.Mocked mocked) { diff --git a/test-framework/junit5-mockito/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback b/test-framework/junit5-mockito/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback new file mode 100644 index 0000000000000..e512f51b2f465 --- /dev/null +++ b/test-framework/junit5-mockito/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback @@ -0,0 +1 @@ +io.quarkus.test.junit.mockito.internal.ResetOuterMockitoMocksCallback diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 17af546e82d81..9858d8e8228a6 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -108,10 +108,12 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.common.http.TestHTTPResourceManager; import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; +import io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback; import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback; import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; import io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback; import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; +import io.quarkus.test.junit.callback.QuarkusTestContext; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.internal.DeepClone; import io.quarkus.test.junit.internal.SerializationWithXStreamFallbackDeepClone; @@ -131,6 +133,8 @@ public class QuarkusTestExtension private static Class actualTestClass; private static Object actualTestInstance; + // needed for @Nested + private static Object outerInstance; private static ClassLoader originalCl; private static RunningQuarkusApplication runningQuarkusApplication; private static Pattern clonePattern; @@ -140,6 +144,7 @@ public class QuarkusTestExtension private static List afterConstructCallbacks; private static List beforeEachCallbacks; private static List afterEachCallbacks; + private static List afterAllCallbacks; private static Class quarkusTestMethodContextClass; private static Class quarkusTestProfile; private static boolean hasPerTestResources; @@ -474,6 +479,7 @@ private void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundExce afterConstructCallbacks = new ArrayList<>(); beforeEachCallbacks = new ArrayList<>(); afterEachCallbacks = new ArrayList<>(); + afterAllCallbacks = new ArrayList<>(); ServiceLoader quarkusTestBeforeClassLoader = ServiceLoader .load(Class.forName(QuarkusTestBeforeClassCallback.class.getName(), false, classLoader), classLoader); @@ -495,6 +501,11 @@ private void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundExce for (Object quarkusTestAfterEach : quarkusTestAfterEachLoader) { afterEachCallbacks.add(quarkusTestAfterEach); } + ServiceLoader quarkusTestAfterAllLoader = ServiceLoader + .load(Class.forName(QuarkusTestAfterAllCallback.class.getName(), false, classLoader), classLoader); + for (Object quarkusTestAfterAll : quarkusTestAfterAllLoader) { + afterAllCallbacks.add(quarkusTestAfterAll); + } } private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) { @@ -640,9 +651,9 @@ public void afterEach(ExtensionContext context) throws Exception { throw new RuntimeException("Could not find method " + originalTestMethod + " on test class"); } - Constructor constructor = quarkusTestMethodContextClass.getConstructor(Object.class, Method.class); + Constructor constructor = quarkusTestMethodContextClass.getConstructor(Object.class, Object.class, Method.class); return new AbstractMap.SimpleEntry<>(quarkusTestMethodContextClass, - constructor.newInstance(actualTestInstance, actualTestMethod)); + constructor.newInstance(actualTestInstance, outerInstance, actualTestMethod)); } private boolean isNativeOrIntegrationTest(Class clazz) { @@ -849,12 +860,13 @@ private void initTestState(ExtensionContext extensionContext, ExtensionState sta Class previousActualTestClass = actualTestClass; actualTestClass = Class.forName(extensionContext.getRequiredTestClass().getName(), true, Thread.currentThread().getContextClassLoader()); + outerInstance = null; if (extensionContext.getRequiredTestClass().isAnnotationPresent(Nested.class)) { - Class parent = actualTestClass.getEnclosingClass(); - Object parentInstance = runningQuarkusApplication.instance(parent); - Constructor declaredConstructor = actualTestClass.getDeclaredConstructor(parent); + Class outerClass = actualTestClass.getEnclosingClass(); + outerInstance = runningQuarkusApplication.instance(outerClass); + Constructor declaredConstructor = actualTestClass.getDeclaredConstructor(outerClass); declaredConstructor.setAccessible(true); - actualTestInstance = declaredConstructor.newInstance(parentInstance); + actualTestInstance = declaredConstructor.newInstance(outerInstance); } else { actualTestInstance = runningQuarkusApplication.instance(actualTestClass); } @@ -868,6 +880,12 @@ private void initTestState(ExtensionContext extensionContext, ExtensionState sta afterConstructCallback.getClass().getMethod("afterConstruct", Object.class).invoke(afterConstructCallback, actualTestInstance); } + if (outerInstance != null) { + for (Object afterConstructCallback : afterConstructCallbacks) { + afterConstructCallback.getClass().getMethod("afterConstruct", Object.class).invoke(afterConstructCallback, + outerInstance); + } + } } catch (Exception e) { throw new TestInstantiationException("Failed to create test instance", e); } @@ -1102,6 +1120,7 @@ private Method determineTCCLExtensionMethod(ReflectiveInvocationContext @Override public void afterAll(ExtensionContext context) throws Exception { resetHangTimeout(); + runAfterAllCallbacks(context); try { if (!isNativeOrIntegrationTest(context.getRequiredTestClass()) && (runningQuarkusApplication != null)) { popMockContext(); @@ -1111,6 +1130,31 @@ public void afterAll(ExtensionContext context) throws Exception { } } finally { currentTestClassStack.pop(); + outerInstance = null; + } + } + + private void runAfterAllCallbacks(ExtensionContext context) throws Exception { + if (isNativeOrIntegrationTest(context.getRequiredTestClass())) { + return; + } + if (afterAllCallbacks.isEmpty()) { + return; + } + + Class quarkusTestContextClass = Class.forName(QuarkusTestContext.class.getName(), true, + runningQuarkusApplication.getClassLoader()); + Object quarkusTestContextInstance = quarkusTestContextClass.getConstructor(Object.class, Object.class) + .newInstance(actualTestInstance, outerInstance); + + ClassLoader original = setCCL(runningQuarkusApplication.getClassLoader()); + try { + for (Object afterAllCallback : afterAllCallbacks) { + afterAllCallback.getClass().getMethod("afterAll", quarkusTestContextClass) + .invoke(afterAllCallback, quarkusTestContextInstance); + } + } finally { + setCCL(original); } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestAfterAllCallback.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestAfterAllCallback.java new file mode 100644 index 0000000000000..867ac995c839c --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestAfterAllCallback.java @@ -0,0 +1,11 @@ +package io.quarkus.test.junit.callback; + +/** + * Can be implemented by classes that shall be called after all test methods in a {@code @QuarkusTest} have been run. + *

+ * The implementing class has to be {@linkplain java.util.ServiceLoader deployed as service provider on the class path}. + */ +public interface QuarkusTestAfterAllCallback { + + void afterAll(QuarkusTestContext context); +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java new file mode 100644 index 0000000000000..8bcb72b69a3fa --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java @@ -0,0 +1,23 @@ +package io.quarkus.test.junit.callback; + +/** + * Context object passed to {@link QuarkusTestAfterAllCallback} + */ +public class QuarkusTestContext { + + private final Object testInstance; + private final Object outerInstance; + + public QuarkusTestContext(Object testInstance, Object outerInstance) { + this.testInstance = testInstance; + this.outerInstance = outerInstance; + } + + public Object getTestInstance() { + return testInstance; + } + + public Object getOuterInstance() { + return outerInstance; + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestMethodContext.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestMethodContext.java index 98f8a5ff92389..6062cf8ae4982 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestMethodContext.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestMethodContext.java @@ -5,20 +5,15 @@ /** * Context object passed to {@link QuarkusTestBeforeEachCallback} and {@link QuarkusTestAfterEachCallback} */ -public final class QuarkusTestMethodContext { +public final class QuarkusTestMethodContext extends QuarkusTestContext { - private final Object testInstance; private final Method testMethod; - public QuarkusTestMethodContext(Object testInstance, Method testMethod) { - this.testInstance = testInstance; + public QuarkusTestMethodContext(Object testInstance, Object outerInstance, Method testMethod) { + super(testInstance, outerInstance); this.testMethod = testMethod; } - public Object getTestInstance() { - return testInstance; - } - public Method getTestMethod() { return testMethod; }