Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QuarkusComponentTest: fix @InjectMock inconsistency #41305

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,10 +1682,11 @@

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`.

Check warning on line 1685 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1685, "column": 17}}}, "severity": "INFO"}
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`.

Check warning on line 1689 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '20.3. Auto Mocking Unsatisfied Dependencies'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '20.3. Auto Mocking Unsatisfied Dependencies'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1689, "column": 198}}}, "severity": "INFO"}

Check warning on line 1689 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1689, "column": 198}}}, "severity": "INFO"}

=== Auto Mocking Unsatisfied Dependencies

Expand All @@ -1693,8 +1694,36 @@
Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency.
The bean has the `@Singleton` scope so it's shared across all injection points with the same required type and qualifiers.
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.

Check warning on line 1697 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1697, "column": 22}}}, "severity": "INFO"}

Check warning on line 1697 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'use' rather than 'leverage' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'use' rather than 'leverage' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1697, "column": 77}}}, "severity": "WARNING"}

[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;
}
}

}
Loading