From 86bba7ab528ffd489a36da88d3ebcd68c2faadab Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 19 Jun 2024 12:17:45 +0200 Subject: [PATCH] QuarkusComponentTest: fix `@InjectMock` inconsistency - take into consideration method params with `@InjectMock` when marking beans as unremovable - document that `@InjectMock` is not intended as a universal replacement for the functionality provided by the Mockito JUnit extension. - also skip param injection for params annotated with `@org.mockito.Mock` so that `@SkipInject` is not needed. - fixes #41224 --- .../asciidoc/getting-started-testing.adoc | 31 +++++++++- .../QuarkusComponentTestExtension.java | 15 ++--- .../mockito/MockitoExtensionTest.java | 58 +++++++++++++++++++ .../paraminject/ParameterInjectMockTest.java | 28 +++++++++ 4 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 25a273ea77dca..3cf9de3d8d005 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1682,7 +1682,8 @@ Finally, the CDI request context is activated and terminated per each test metho Test class fields annotated with `@jakarta.inject.Inject` and `@io.quarkus.test.InjectMock` are injected after a test instance is created. Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed. -Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject`. +Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject` or `@org.mockito.Mock`. +There are also some JUnit built-in parameters, such as `RepetitionInfo` and `TestInfo`, which are skipped automatically. Dependent beans injected into the test method arguments are correctly destroyed after the test method completes. NOTE: Arguments of a `@ParameterizedTest` method that are provided by an `ArgumentsProvider`, for example with `@org.junit.jupiter.params.provider.ValueArgumentsProvider`, must be annotated with `@SkipInject`. @@ -1695,6 +1696,34 @@ The bean has the `@Singleton` scope so it's shared across all injection points w The injected reference is an _unconfigured_ Mockito mock. You can inject the mock in your test using the `io.quarkus.test.InjectMock` annotation and leverage the Mockito API to configure the behavior. +[NOTE] +==== +`@InjectMock` is not intended as a universal replacement for functionality provided by the Mockito JUnit extension. +It's meant to be used for configuration of unsatisfied dependencies of CDI beans. +You can use the `QuarkusComponentTest` and `MockitoExtension` side by side. + +[source, java] +---- +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@QuarkusComponentTest +public class FooTest { + + @TestConfigProperty(key = "bar", value = "true") + @Test + public void testPing(Foo foo, @InjectMock Charlie charlieMock, @Mock Ping ping) { + Mockito.when(ping.pong()).thenReturn("OK"); + Mockito.when(charlieMock.ping()).thenReturn(ping); + assertEquals("OK", foo.ping()); + } +} +---- + +==== + === Custom Mocks For Unsatisfied Dependencies Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. 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 cd143b17d9abd..fd6dc7f0877f4 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 @@ -85,6 +85,7 @@ import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.Mock; import io.quarkus.arc.All; import io.quarkus.arc.Arc; @@ -279,6 +280,8 @@ && isTestMethod(parameterContext.getDeclaringExecutable()) // A method/param annotated with @SkipInject is never supported && !parameterContext.isAnnotated(SkipInject.class) && !parameterContext.getDeclaringExecutable().isAnnotationPresent(SkipInject.class) + // A param annotated with @org.mockito.Mock is never supported + && !parameterContext.isAnnotated(Mock.class) // Skip params covered by built-in extensions && !BUILTIN_PARAMETER.test(parameterContext.getParameter())) { BeanManager beanManager = Arc.container().beanManager(); @@ -498,15 +501,9 @@ private static Set getQualifiers(AnnotatedElement element, C } private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusComponentTestConfiguration configuration) { - Class testClass = extensionContext.getRequiredTestClass(); - // Collect all component injection points to define a bean removal exclusion - List injectFields = findInjectFields(testClass); - List injectParams = findInjectParams(testClass); - if (configuration.componentClasses.isEmpty()) { throw new IllegalStateException("No component classes to test"); } - // Make sure Arc is down try { Arc.shutdown(); @@ -528,6 +525,7 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC throw new IllegalStateException("Failed to create index", e); } + Class testClass = extensionContext.getRequiredTestClass(); ClassLoader testClassClassLoader = testClass.getClassLoader(); // The test class is loaded by the QuarkusClassLoader in continuous testing environment boolean isContinuousTesting = testClassClassLoader instanceof QuarkusClassLoader; @@ -543,6 +541,10 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC Set interceptorBindings = new HashSet<>(); AtomicReference beanResolver = new AtomicReference<>(); + // Collect all @Inject and @InjectMock test class injection points to define a bean removal exclusion + List injectFields = findInjectFields(testClass); + List injectParams = findInjectParams(testClass); + BeanProcessor.Builder builder = BeanProcessor.builder() .setName(testClass.getName().replace('.', '_')) .addRemovalExclusion(b -> { @@ -1010,7 +1012,6 @@ private List findInjectParams(Class testClass) { for (Method method : testMethods) { for (Parameter param : method.getParameters()) { if (BUILTIN_PARAMETER.test(param) - || param.isAnnotationPresent(InjectMock.class) || param.isAnnotationPresent(SkipInject.class)) { continue; } diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java new file mode 100644 index 0000000000000..be71bcb797aea --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java @@ -0,0 +1,58 @@ +package io.quarkus.test.component.mockito; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; + +@QuarkusComponentTest +public class MockitoExtensionTest { + + // Bar - component under test, real bean + // Baz - mock of the synthetic bean registered to satisfy Bar#baz + // Foo - plain Mockito mock + @ExtendWith(MockitoExtension.class) + @Test + public void testInjectMock(Bar bar, @InjectMock Baz baz, @Mock Foo foo) { + Mockito.when(foo.pong()).thenReturn(false); + Mockito.when(baz.ping()).thenReturn(foo); + assertFalse(bar.ping().pong()); + } + + @Singleton + public static class Bar { + + @Inject + Baz baz; + + Foo ping() { + return baz.ping(); + } + + } + + public static class Baz { + + Foo ping() { + return null; + } + + } + + public static class Foo { + + boolean pong() { + return true; + } + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java new file mode 100644 index 0000000000000..3cba305ec54d4 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java @@ -0,0 +1,28 @@ +package io.quarkus.test.component.paraminject; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; + +@QuarkusComponentTest +public class ParameterInjectMockTest { + + // Foo is mocked even if it's not a dependency of a tested component + @Test + public void testInjectMock(@InjectMock MyFoo foo) { + Mockito.when(foo.ping()).thenReturn(false); + assertFalse(foo.ping()); + } + + public static class MyFoo { + + boolean ping() { + return true; + } + } + +}