From 1046d339abf400bc69999902a469cf37ffc6a8bc Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 28 Aug 2023 17:51:40 +0200 Subject: [PATCH] Mockito testing - manually activate req. context when creating Mockito spies --- .../it/mockbean/RequestScopedSpyTest.java | 65 +++++++++++++++++++ .../internal/CreateMockitoSpiesCallback.java | 32 ++++++++- ...junit.callback.QuarkusTestAfterAllCallback | 1 + 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedSpyTest.java diff --git a/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedSpyTest.java b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedSpyTest.java new file mode 100644 index 0000000000000..b0a0e8e99e262 --- /dev/null +++ b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedSpyTest.java @@ -0,0 +1,65 @@ +package io.quarkus.it.mockbean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +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.InjectSpy; + +/** + * Tests that Mockito spies can be RequestScoped - this case is different from app scoped/singletons because by the time + * we create spies (right after creating test instance), we need to manually activate req. context for this creation. + */ +@QuarkusTest +public class RequestScopedSpyTest { + + @InjectSpy + private RequestBean spiedBean; + + @Inject + private SomeOtherBean injectedBean; + + @Test + void verifySpyWorks() { + // Executes gracefully + assertNotNull(spiedBean); + injectedBean.pong(); + Mockito.verify(spiedBean, Mockito.times(1)).ping(); + } + + @Nested + class NestedTest { + @Test + void verifyNestedSpyWorks() { + assertNotNull(spiedBean); + injectedBean.pong(); + Mockito.verify(spiedBean, Mockito.times(1)).ping(); + } + } + + @RequestScoped + public static class RequestBean { + + public void ping() { + + } + } + + @ApplicationScoped + public static class SomeOtherBean { + + @Inject + RequestBean bean; + + public void pong() { + bean.ping(); + } + } +} diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoSpiesCallback.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoSpiesCallback.java index 8db9c5b31ec46..1aaea0ba7ccff 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoSpiesCallback.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoSpiesCallback.java @@ -1,20 +1,34 @@ package io.quarkus.test.junit.mockito.internal; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; import org.mockito.AdditionalAnswers; import org.mockito.Mockito; +import io.quarkus.arc.Arc; import io.quarkus.arc.ClientProxy; +import io.quarkus.arc.InjectableContext; import io.quarkus.arc.InstanceHandle; +import io.quarkus.test.junit.callback.QuarkusTestAfterAllCallback; import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback; +import io.quarkus.test.junit.callback.QuarkusTestContext; import io.quarkus.test.junit.mockito.InjectSpy; -public class CreateMockitoSpiesCallback implements QuarkusTestAfterConstructCallback { +public class CreateMockitoSpiesCallback implements QuarkusTestAfterConstructCallback, QuarkusTestAfterAllCallback { + + // in nested tests, there are multiple states created before destruction is triggered + private static Set statesToDestroy = new HashSet<>(); @Override public void afterConstruct(Object testInstance) { Class current = testInstance.getClass(); + // QuarkusTestAfterConstructCallback can be used in @QuarkusIntegrationTest where there is no Arc + boolean contextPreviouslyActive = Arc.container() != null && Arc.container().requestContext().isActive(); + if (!contextPreviouslyActive) { + Arc.container().requestContext().activate(); + } while (current.getSuperclass() != null) { for (Field field : current.getDeclaredFields()) { InjectSpy injectSpyAnnotation = field.getAnnotation(InjectSpy.class); @@ -28,16 +42,21 @@ public void afterConstruct(Object testInstance) { } current = current.getSuperclass(); } + if (!contextPreviouslyActive) { + // only deactivate; we will destroy them in QuarkusTestAfterAllCallback + statesToDestroy.add(Arc.container().requestContext().getState()); + Arc.container().requestContext().deactivate(); + } } private Object createSpyAndSetTestField(Object testInstance, Field field, InstanceHandle beanHandle, boolean delegate) { Object spy; + // Unwrap the client proxy if needed Object contextualInstance = ClientProxy.unwrap(beanHandle.get()); if (delegate) { spy = Mockito.mock(beanHandle.getBean().getImplementationClass(), AdditionalAnswers.delegatesTo(contextualInstance)); } else { - // Unwrap the client proxy if needed spy = Mockito.spy(contextualInstance); } field.setAccessible(true); @@ -49,4 +68,13 @@ private Object createSpyAndSetTestField(Object testInstance, Field field, Instan return spy; } + @Override + public void afterAll(QuarkusTestContext context) { + if (!statesToDestroy.isEmpty()) { + for (InjectableContext.ContextState state : statesToDestroy) { + Arc.container().requestContext().destroy(state); + } + statesToDestroy.clear(); + } + } } 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 index e512f51b2f465..d08dfeb9317a8 100644 --- 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 @@ -1 +1,2 @@ io.quarkus.test.junit.mockito.internal.ResetOuterMockitoMocksCallback +io.quarkus.test.junit.mockito.internal.CreateMockitoSpiesCallback \ No newline at end of file