Skip to content

Commit

Permalink
Inject mock annotations cleanup
Browse files Browse the repository at this point in the history
- introduce io.quarkus.test.InjectMock
- deprecate io.quakus.test.junit.mockito.InjectMock
- remove io.quarkus.test.component.ConfigureMock
  • Loading branch information
mkouba committed Jun 15, 2023
1 parent c7c7370 commit 610a4a5
Show file tree
Hide file tree
Showing 20 changed files with 186 additions and 58 deletions.
18 changes: 9 additions & 9 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -1562,7 +1562,7 @@ public class FooTest {
@Inject
Foo foo; <3>
@ConfigureMock
@InjectMock
Charlie charlieMock; <4>
@Test
Expand All @@ -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;
Expand All @@ -1599,7 +1599,7 @@ public class FooTest {
@Inject
Foo foo;
@ConfigureMock
@InjectMock
Charlie charlieMock;
@Test
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* This annotation is supported:
* <ul>
* <li>in a {@code io.quarkus.test.component.QuarkusComponentTest},</li>
* <li>in a {@code io.quarkus.test.QuarkusTest} if {@code quarkus-junit5-mockito} is present.</li>
* </ul>
* The lifecycle and configuration API of the injected mock depends on the type of test being used.
* <p>
* Some test types impose additional restrictons and limitations. For example, only beans that have a
* <a href="https://quarkus.io/guides/cdi#client_proxies">client proxy</a> may be mocked in a
* {@code io.quarkus.test.junit.QuarkusTest}.
*/
@Retention(RUNTIME)
@Target(FIELD)
public @interface InjectMock {

}
4 changes: 4 additions & 0 deletions test-framework/junit5-component/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-common</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.common</groupId>
<artifactId>smallrye-common-annotation</artifactId>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -753,11 +754,11 @@ static <T> T cast(Object obj) {

private List<FieldInjector> injectFields(Class<?> testClass, Object testInstance) throws Exception {
List<Class<? extends Annotation>> injectAnnotations;
Class<? extends Annotation> injectMock = loadInjectMock();
if (injectMock != null) {
injectAnnotations = List.of(Inject.class, ConfigureMock.class, injectMock);
Class<? extends Annotation> 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<FieldInjector> injectedFields = new ArrayList<>();
for (Field field : testClass.getDeclaredFields()) {
Expand Down Expand Up @@ -839,7 +840,7 @@ void unset(Object testInstance) throws Exception {
}

@SuppressWarnings("unchecked")
private Class<? extends Annotation> loadInjectMock() {
private Class<? extends Annotation> loadDeprecatedInjectMock() {
try {
return (Class<? extends Annotation>) Class.forName("io.quarkus.test.junit.mockito.InjectMock");
} catch (Throwable e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,7 +22,7 @@ public class DependencyMockingTest {
@Inject
MyComponent myComponent;

@ConfigureMock
@InjectMock
Charlie charlie;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,7 +25,7 @@ public class MockConfiguratorTest {
@Inject
MyComponent myComponent;

@ConfigureMock
@InjectMock
Charlie charlie;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mockito;

import io.quarkus.test.InjectMock;

public class MockNotSharedForClassHierarchyTest {

@RegisterExtension
Expand All @@ -18,7 +20,7 @@ public class MockNotSharedForClassHierarchyTest {
@Inject
Component component;

@ConfigureMock
@InjectMock
Alpha alpha;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,7 +22,7 @@ public class ObserverInjectingMockTest {
@Inject
Event<Boolean> event;

@ConfigureMock
@InjectMock
Delta delta;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,7 +21,7 @@ public class ProgrammaticLookupMockTest {
@Inject
ProgrammaticLookComponent component;

@ConfigureMock
@InjectMock
Delta delta;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +20,7 @@ public class DeclarativeDependencyMockingTest {
@Inject
MyComponent myComponent;

@ConfigureMock
@InjectMock
Charlie charlie;

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,10 +24,10 @@ public class ListAllMockTest {
@Inject
ListAllComponent component;

@ConfigureMock
@InjectMock
Delta delta;

@ConfigureMock
@InjectMock
@SimpleQualifier
Bravo bravo;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -24,21 +25,33 @@ 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<Object> 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);
}
}
}
current = current.getSuperclass();
}
}

private void injectField(Object testInstance, Field field, Class<? extends Annotation> annotationType,
boolean returnsDeepMocks) {
InstanceHandle<?> beanHandle = getBeanHandle(testInstance, field, annotationType);
Optional<Object> result = createMockAndSetTestField(testInstance, field, beanHandle,
new MockConfiguration(returnsDeepMocks));
if (result.isPresent()) {
MockitoMocksTracker.track(testInstance, result.get(), beanHandle.get());
}
}

private Optional<Object> createMockAndSetTestField(Object testInstance, Field field, InstanceHandle<?> beanHandle,
MockConfiguration mockConfiguration) {
Class<?> implementationClass = beanHandle.getBean().getImplementationClass();
Expand Down
Loading

0 comments on commit 610a4a5

Please sign in to comment.