From 610a4a549c138118e9d16219ecbe357a23da63d3 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 9 Jun 2023 14:23:08 +0200 Subject: [PATCH] Inject mock annotations cleanup - introduce io.quarkus.test.InjectMock - deprecate io.quakus.test.junit.mockito.InjectMock - remove io.quarkus.test.component.ConfigureMock --- .../asciidoc/getting-started-testing.adoc | 18 +++---- .../it/mockbean/RequestScopedFooMockTest.java | 2 +- .../main/java/io/quarkus/test/InjectMock.java | 27 +++++++++++ test-framework/junit5-component/pom.xml | 4 ++ .../quarkus/test/component/ConfigureMock.java | 16 ------- .../QuarkusComponentTestExtension.java | 11 +++-- .../test/component/DependencyMockingTest.java | 3 +- .../test/component/MockConfiguratorTest.java | 3 +- .../MockNotSharedForClassHierarchyTest.java | 4 +- .../component/ObserverInjectingMockTest.java | 3 +- .../component/ProgrammaticLookupMockTest.java | 3 +- .../DeclarativeDependencyMockingTest.java | 4 +- .../declarative/ListAllMockTest.java | 6 +-- .../test/junit/mockito/InjectMock.java | 5 +- .../test/junit/mockito/MockitoConfig.java | 34 ++++++++++++++ .../internal/CreateMockitoMocksCallback.java | 29 ++++++++---- ...copedTestBuildChainCustomizerProducer.java | 14 ++++-- ...eMockTestBuildChainCustomizerProducer.java | 10 ++-- .../internal/VerifyMockitoMocksCallback.java | 47 +++++++++++++++++++ ...callback.QuarkusTestAfterConstructCallback | 1 + 20 files changed, 186 insertions(+), 58 deletions(-) create mode 100644 test-framework/common/src/main/java/io/quarkus/test/InjectMock.java delete mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java create mode 100644 test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/MockitoConfig.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/VerifyMockitoMocksCallback.java create mode 100644 test-framework/junit5/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index cf621d16d3c22..5332aaa6093ba 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -828,13 +828,13 @@ Note that there is no dependency on Mockito, you can use any mocking library you objects to provide the behaviour you require. NOTE: Using `@Inject` will get you a CDI proxy to the mock instance you install, which is not suitable for passing to methods such as `Mockito.verify` -which want the mock instance itself. So if you need to call methods such as `verify` you need to hang on to the mock instance in your test, or use `@InjectMock` -as shown below. +which want the mock instance itself. +So if you need to call methods such as `verify` you should hang on to the mock instance in your test, or use `@io.quarkus.test.InjectMock`. ==== Further simplification with `@InjectMock` Building on the features provided by `QuarkusMock`, Quarkus also allows users to effortlessly take advantage of link:https://site.mockito.org/[Mockito] for mocking the beans supported by `QuarkusMock`. -This functionality is available via the `@io.quarkus.test.junit.mockito.InjectMock` annotation which is available in the `quarkus-junit5-mockito` dependency. +This functionality is available with the `@io.quarkus.test.InjectMock` annotation if the `quarkus-junit5-mockito` dependency is present. Using `@InjectMock`, the previous example could be written as follows: @@ -938,7 +938,7 @@ public class MockGreetingServiceTest { <1> Since we configured `greetingService` as a mock, the `GreetingResource` which uses the `GreetingService` bean, we get the mocked response instead of the response of the regular `GreetingService` bean By default, the `@InjectMock` annotation can be used for any normal CDI scoped bean (e.g. `@ApplicationScoped`, `@RequestScoped`). -Mocking `@Singleton` beans can be performed by setting the `convertScopes` property to true (such as `@InjectMock(convertScopes = true`). +Mocking `@Singleton` beans can be performed by adding the `@MockitoConfig(convertScopes = true)` annotation. This will convert the `@Singleton` bean to an `@ApplicationScoped` bean for the test. This is considered an advanced option and should only be performed if you fully understand the consequences of changing the scope of the bean. @@ -1549,8 +1549,8 @@ Then a component test could look like: import static org.junit.jupiter.api.Assertions.assertEquals; import jakarta.inject.Inject; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.TestConfigProperty; -import io.quarkus.test.component.TestMock; import io.quarkus.test.component.QuarkusComponentTest; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -1562,7 +1562,7 @@ public class FooTest { @Inject Foo foo; <3> - @ConfigureMock + @InjectMock Charlie charlieMock; <4> @Test @@ -1586,7 +1586,7 @@ The test above could be rewritten like: import static org.junit.jupiter.api.Assertions.assertEquals; import jakarta.inject.Inject; -import io.quarkus.test.component.TestMock; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.QuarkusComponentTestExtension; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -1599,7 +1599,7 @@ public class FooTest { @Inject Foo foo; - @ConfigureMock + @InjectMock Charlie charlieMock; @Test @@ -1616,7 +1616,7 @@ public class FooTest { So what exactly does the `QuarkusComponentTest` do? It starts the CDI container and registers a dedicated xref:config-reference.adoc[configuration object] during the `before all` test phase. The container is stopped and the config is released during the `after all` test phase. -The fields annotated with `@Inject` and `@ConfigureMock` are injected after a test instance is created and unset before a test instance is destroyed. +The fields annotated with `@Inject` and `@InjectMock` are injected after a test instance is created and unset before a test instance is destroyed. Finally, the CDI request context is activated and terminated per each test method. NOTE: By default, a new test instance is created for each test method. Therefore, a new CDI container is started for each test method. However, if the test class is annotated with `@org.junit.jupiter.api.TestInstance` and the test instance lifecycle is set to `org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS` then the CDI container will be shared across all test method executions of a given test class. diff --git a/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedFooMockTest.java b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedFooMockTest.java index c8f369233216a..c36b0f54fe566 100644 --- a/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedFooMockTest.java +++ b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/RequestScopedFooMockTest.java @@ -6,8 +6,8 @@ import org.junit.jupiter.api.Test; +import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; @QuarkusTest class RequestScopedFooMockTest { diff --git a/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java b/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java new file mode 100644 index 0000000000000..892f52f253786 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java @@ -0,0 +1,27 @@ +package io.quarkus.test; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Instructs the test engine to inject a mock instance of a bean in the field of a test class. + *

+ * This annotation is supported: + *

+ * The lifecycle and configuration API of the injected mock depends on the type of test being used. + *

+ * Some test types impose additional restrictons and limitations. For example, only beans that have a + * client proxy may be mocked in a + * {@code io.quarkus.test.junit.QuarkusTest}. + */ +@Retention(RUNTIME) +@Target(FIELD) +public @interface InjectMock { + +} diff --git a/test-framework/junit5-component/pom.xml b/test-framework/junit5-component/pom.xml index 934ec6194b6df..4c2f6090b01ea 100644 --- a/test-framework/junit5-component/pom.xml +++ b/test-framework/junit5-component/pom.xml @@ -50,6 +50,10 @@ io.quarkus quarkus-core + + io.quarkus + quarkus-test-common + io.smallrye.common smallrye-common-annotation diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java deleted file mode 100644 index b545c0b30362f..0000000000000 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.quarkus.test.component; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Marks a field of a test class as a target of a mock dependency injection. - */ -@Retention(RUNTIME) -@Target(ElementType.FIELD) -public @interface ConfigureMock { - -} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index dcba1a67418dc..51e3bec34d887 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -87,6 +87,7 @@ import io.quarkus.arc.processor.Types; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader; +import io.quarkus.test.InjectMock; import io.smallrye.common.annotation.Experimental; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -753,11 +754,11 @@ static T cast(Object obj) { private List injectFields(Class testClass, Object testInstance) throws Exception { List> injectAnnotations; - Class injectMock = loadInjectMock(); - if (injectMock != null) { - injectAnnotations = List.of(Inject.class, ConfigureMock.class, injectMock); + Class deprecatedInjectMock = loadDeprecatedInjectMock(); + if (deprecatedInjectMock != null) { + injectAnnotations = List.of(Inject.class, InjectMock.class, deprecatedInjectMock); } else { - injectAnnotations = List.of(Inject.class, ConfigureMock.class); + injectAnnotations = List.of(Inject.class, InjectMock.class); } List injectedFields = new ArrayList<>(); for (Field field : testClass.getDeclaredFields()) { @@ -839,7 +840,7 @@ void unset(Object testInstance) throws Exception { } @SuppressWarnings("unchecked") - private Class loadInjectMock() { + private Class loadDeprecatedInjectMock() { try { return (Class) Class.forName("io.quarkus.test.junit.mockito.InjectMock"); } catch (Throwable e) { diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java index 017307b14bca3..a28f305cd93e3 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.beans.Charlie; import io.quarkus.test.component.beans.MyComponent; @@ -21,7 +22,7 @@ public class DependencyMockingTest { @Inject MyComponent myComponent; - @ConfigureMock + @InjectMock Charlie charlie; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java index f8daa8eb1b172..c18d9e44fb8f5 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.beans.Charlie; import io.quarkus.test.component.beans.MyComponent; @@ -24,7 +25,7 @@ public class MockConfiguratorTest { @Inject MyComponent myComponent; - @ConfigureMock + @InjectMock Charlie charlie; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java index 68901c0a6cfc6..982250daaa7ed 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; +import io.quarkus.test.InjectMock; + public class MockNotSharedForClassHierarchyTest { @RegisterExtension @@ -18,7 +20,7 @@ public class MockNotSharedForClassHierarchyTest { @Inject Component component; - @ConfigureMock + @InjectMock Alpha alpha; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java index 3abeb552d0f5f..af369b857d0cc 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.beans.Delta; import io.quarkus.test.component.beans.MyComponent; @@ -21,7 +22,7 @@ public class ObserverInjectingMockTest { @Inject Event event; - @ConfigureMock + @InjectMock Delta delta; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java index 7bd43fcf3aae1..073f14a9c79ec 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.beans.Delta; public class ProgrammaticLookupMockTest { @@ -20,7 +21,7 @@ public class ProgrammaticLookupMockTest { @Inject ProgrammaticLookComponent component; - @ConfigureMock + @InjectMock Delta delta; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java index d71eeef0a2b9a..7ff08de26db8f 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.quarkus.test.component.ConfigureMock; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.QuarkusComponentTest; import io.quarkus.test.component.TestConfigProperty; import io.quarkus.test.component.beans.Charlie; @@ -20,7 +20,7 @@ public class DeclarativeDependencyMockingTest { @Inject MyComponent myComponent; - @ConfigureMock + @InjectMock Charlie charlie; @Test diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java index ddee8d2309ab3..5534cc64c707a 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java @@ -12,7 +12,7 @@ import org.mockito.Mockito; import io.quarkus.arc.All; -import io.quarkus.test.component.ConfigureMock; +import io.quarkus.test.InjectMock; import io.quarkus.test.component.QuarkusComponentTest; import io.quarkus.test.component.beans.Bravo; import io.quarkus.test.component.beans.Delta; @@ -24,10 +24,10 @@ public class ListAllMockTest { @Inject ListAllComponent component; - @ConfigureMock + @InjectMock Delta delta; - @ConfigureMock + @InjectMock @SimpleQualifier Bravo bravo; diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/InjectMock.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/InjectMock.java index d5d71839015cd..209ec79b45d01 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/InjectMock.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/InjectMock.java @@ -7,8 +7,11 @@ /** * When used on a field of a test class, the field becomes a Mockito mock, - * that is then used to mock the normal scoped bean which the field represents + * that is then used to mock the normal scoped bean which the field represents. + * + * @deprecated Use {@link io.quarkus.test.InjectMock} and {@link MockitoConfig} instead. */ +@Deprecated(since = "3.2", forRemoval = true) @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface InjectMock { diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/MockitoConfig.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/MockitoConfig.java new file mode 100644 index 0000000000000..44077d60510df --- /dev/null +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/MockitoConfig.java @@ -0,0 +1,34 @@ +package io.quarkus.test.junit.mockito; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.test.InjectMock; + +/** + * This annotation can be used to configure a Mockito mock injected in a field of a test class that is annotated with + * {@link InjectMock}. This annotation is only supported in a {@code io.quarkus.test.QuarkusTest}. + * + * @see InjectMock + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface MockitoConfig { + + /** + * If true, then Quarkus will change the scope of the target {@code Singleton} bean to {@code ApplicationScoped} + * to make it mockable. + *

+ * This is an advanced setting and should only be used if you don't rely on the differences between {@code Singleton} + * and {@code ApplicationScoped} beans (for example it is invalid to read fields of {@code ApplicationScoped} beans + * as a proxy stands in place of the actual implementation) + */ + boolean convertScopes() default false; + + /** + * If true, the mock will be created with the {@link org.mockito.Mockito#RETURNS_DEEP_STUBS} + */ + boolean returnsDeepMocks() default false; +} diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java index c1998bae88970..99bfce1995b5e 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java @@ -16,6 +16,7 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback; import io.quarkus.test.junit.mockito.InjectMock; +import io.quarkus.test.junit.mockito.MockitoConfig; public class CreateMockitoMocksCallback implements QuarkusTestAfterConstructCallback { @@ -24,14 +25,16 @@ public void afterConstruct(Object testInstance) { Class current = testInstance.getClass(); while (current.getSuperclass() != null) { for (Field field : current.getDeclaredFields()) { - InjectMock injectMockAnnotation = field.getAnnotation(InjectMock.class); - if (injectMockAnnotation != null) { - boolean returnsDeepMocks = injectMockAnnotation.returnsDeepMocks(); - InstanceHandle beanHandle = getBeanHandle(testInstance, field, InjectMock.class); - Optional result = createMockAndSetTestField(testInstance, field, beanHandle, - new MockConfiguration(returnsDeepMocks)); - if (result.isPresent()) { - MockitoMocksTracker.track(testInstance, result.get(), beanHandle.get()); + InjectMock deprecatedInjectMock = field.getAnnotation(InjectMock.class); + if (deprecatedInjectMock != null) { + boolean returnsDeepMocks = deprecatedInjectMock.returnsDeepMocks(); + injectField(testInstance, field, InjectMock.class, returnsDeepMocks); + } else { + io.quarkus.test.InjectMock injectMock = field.getAnnotation(io.quarkus.test.InjectMock.class); + if (injectMock != null) { + MockitoConfig config = field.getAnnotation(MockitoConfig.class); + boolean returnsDeepMocks = config != null ? config.returnsDeepMocks() : false; + injectField(testInstance, field, io.quarkus.test.InjectMock.class, returnsDeepMocks); } } } @@ -39,6 +42,16 @@ public void afterConstruct(Object testInstance) { } } + private void injectField(Object testInstance, Field field, Class annotationType, + boolean returnsDeepMocks) { + InstanceHandle beanHandle = getBeanHandle(testInstance, field, annotationType); + Optional result = createMockAndSetTestField(testInstance, field, beanHandle, + new MockConfiguration(returnsDeepMocks)); + if (result.isPresent()) { + MockitoMocksTracker.track(testInstance, result.get(), beanHandle.get()); + } + } + private Optional createMockAndSetTestField(Object testInstance, Field field, InstanceHandle beanHandle, MockConfiguration mockConfiguration) { Class implementationClass = beanHandle.getBean().getImplementationClass(); diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SingletonToApplicationScopedTestBuildChainCustomizerProducer.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SingletonToApplicationScopedTestBuildChainCustomizerProducer.java index 9906fc368dec3..c96ad9da4860d 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SingletonToApplicationScopedTestBuildChainCustomizerProducer.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/SingletonToApplicationScopedTestBuildChainCustomizerProducer.java @@ -27,11 +27,14 @@ import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.junit.mockito.InjectSpy; +import io.quarkus.test.junit.mockito.MockitoConfig; public class SingletonToApplicationScopedTestBuildChainCustomizerProducer implements TestBuildChainCustomizerProducer { - private static final DotName INJECT_MOCK = DotName.createSimple(InjectMock.class.getName()); - private static final DotName INJECT_SPY = DotName.createSimple(InjectSpy.class.getName()); + static final DotName INJECT_MOCK = DotName.createSimple(io.quarkus.test.InjectMock.class.getName()); + static final DotName DEPRECATED_INJECT_MOCK = DotName.createSimple(InjectMock.class.getName()); + static final DotName INJECT_SPY = DotName.createSimple(InjectSpy.class.getName()); + static final DotName MOCKITO_CONFIG = DotName.createSimple(MockitoConfig.class.getName()); @Override public Consumer produce(Index testClassesIndex) { @@ -44,12 +47,17 @@ public void accept(BuildChainBuilder buildChainBuilder) { public void execute(BuildContext context) { Set mockTypes = new HashSet<>(); List instances = new ArrayList<>(); - instances.addAll(testClassesIndex.getAnnotations(INJECT_MOCK)); + instances.addAll(testClassesIndex.getAnnotations(DEPRECATED_INJECT_MOCK)); instances.addAll(testClassesIndex.getAnnotations(INJECT_SPY)); + instances.addAll(testClassesIndex.getAnnotations(MOCKITO_CONFIG)); for (AnnotationInstance instance : instances) { if (instance.target().kind() != AnnotationTarget.Kind.FIELD) { continue; } + if (instance.name().equals(MOCKITO_CONFIG) + && instance.target().asField().hasAnnotation(INJECT_MOCK)) { + continue; + } AnnotationValue allowScopeConversionValue = instance.value("convertScopes"); if ((allowScopeConversionValue != null) && allowScopeConversionValue.asBoolean()) { // we need to fetch the type of the bean, so we need to look at the type of the field diff --git a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/UnremoveableMockTestBuildChainCustomizerProducer.java b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/UnremoveableMockTestBuildChainCustomizerProducer.java index ae1f2a32db189..2cf787127ca75 100644 --- a/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/UnremoveableMockTestBuildChainCustomizerProducer.java +++ b/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/UnremoveableMockTestBuildChainCustomizerProducer.java @@ -1,12 +1,12 @@ package io.quarkus.test.junit.mockito.internal; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -14,12 +14,9 @@ import io.quarkus.builder.BuildContext; import io.quarkus.builder.BuildStep; import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; -import io.quarkus.test.junit.mockito.InjectMock; public class UnremoveableMockTestBuildChainCustomizerProducer implements TestBuildChainCustomizerProducer { - private static final DotName INJECT_MOCK = DotName.createSimple(InjectMock.class.getName()); - @Override public Consumer produce(Index testClassesIndex) { return new Consumer() { @@ -30,7 +27,10 @@ public void accept(BuildChainBuilder buildChainBuilder) { @Override public void execute(BuildContext context) { Set mockTypes = new HashSet<>(); - List instances = testClassesIndex.getAnnotations(INJECT_MOCK); + List instances = new ArrayList<>(testClassesIndex.getAnnotations( + SingletonToApplicationScopedTestBuildChainCustomizerProducer.DEPRECATED_INJECT_MOCK)); + instances.addAll(testClassesIndex + .getAnnotations(SingletonToApplicationScopedTestBuildChainCustomizerProducer.INJECT_MOCK)); for (AnnotationInstance instance : instances) { mockTypes.add(instance.target().asField().type().name().toString()); } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/VerifyMockitoMocksCallback.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/VerifyMockitoMocksCallback.java new file mode 100644 index 0000000000000..02d1f28212646 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/VerifyMockitoMocksCallback.java @@ -0,0 +1,47 @@ +package io.quarkus.test.junit.internal; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback; + +/** + * This callback verifies that {@code @io.quarkus.test.InjectMock} is not declared on a field of a {@code @QuarkusTest} + * unless the {@code quarkus-junit5-mockito} is present. + */ +public class VerifyMockitoMocksCallback implements QuarkusTestAfterConstructCallback { + + @Override + public void afterConstruct(Object testInstance) { + Class deprecatedInjectMock = loadDeprecatedInjectMock(); + if (deprecatedInjectMock == null) { + List injectMockFields = new ArrayList<>(); + Class current = testInstance.getClass(); + while (current != null) { + for (Field field : current.getDeclaredFields()) { + if (field.isAnnotationPresent(InjectMock.class)) { + injectMockFields.add(field); + } + } + current = current.getSuperclass(); + } + if (!injectMockFields.isEmpty()) { + throw new IllegalStateException( + "@io.quarkus.test.InjectMock declared on one or more fields of a @QuarkusTest but the quarkus-junit5-mockito dependency is not present: " + + injectMockFields.stream().map(f -> "/n/t- " + f.toString())); + } + } + } + + @SuppressWarnings("unchecked") + private Class loadDeprecatedInjectMock() { + try { + return (Class) Class.forName("io.quarkus.test.junit.mockito.InjectMock"); + } catch (Throwable e) { + return null; + } + } +} diff --git a/test-framework/junit5/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback b/test-framework/junit5/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback new file mode 100644 index 0000000000000..4dbd4d8242b69 --- /dev/null +++ b/test-framework/junit5/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback @@ -0,0 +1 @@ +io.quarkus.test.junit.internal.VerifyMockitoMocksCallback