Skip to content

Commit

Permalink
Merge pull request quarkusio#41305 from mkouba/issue-41224
Browse files Browse the repository at this point in the history
QuarkusComponentTest: fix `@InjectMock` inconsistency
  • Loading branch information
mkouba authored Jun 19, 2024
2 parents 112c7c3 + 86bba7a commit ed77ee2
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 8 deletions.
31 changes: 30 additions & 1 deletion docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -498,15 +501,9 @@ private static Set<AnnotationInstance> 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<Field> injectFields = findInjectFields(testClass);
List<Parameter> injectParams = findInjectParams(testClass);

if (configuration.componentClasses.isEmpty()) {
throw new IllegalStateException("No component classes to test");
}

// Make sure Arc is down
try {
Arc.shutdown();
Expand All @@ -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;
Expand All @@ -543,6 +541,10 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC
Set<String> interceptorBindings = new HashSet<>();
AtomicReference<BeanResolver> beanResolver = new AtomicReference<>();

// Collect all @Inject and @InjectMock test class injection points to define a bean removal exclusion
List<Field> injectFields = findInjectFields(testClass);
List<Parameter> injectParams = findInjectParams(testClass);

BeanProcessor.Builder builder = BeanProcessor.builder()
.setName(testClass.getName().replace('.', '_'))
.addRemovalExclusion(b -> {
Expand Down Expand Up @@ -1010,7 +1012,6 @@ private List<Parameter> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}

}

0 comments on commit ed77ee2

Please sign in to comment.