Skip to content

Commit ba692aa

Browse files
committed
Honor MockReset without @⁠MockitoBean or @⁠MockitoSpyBean fields
Prior to this commit, the static factory methods in MockReset (such as MockReset.before() and MockReset.after()) could only be applied to beans within the ApplicationContext if the test class declared at least one field annotated with either @⁠MockitoBean or @⁠MockitoSpyBean. However, the Javadoc states that it should be possible to apply MockReset directly to any mock in the ApplicationContext using the static methods in MockReset. To address that, this commit reworks the "enabled" logic in MockitoResetTestExecutionListener as follows. - We no longer check for the presence of annotations from the org.springframework.test.context.bean.override.mockito package to determine if MockReset is enabled. - Instead, we now rely on a new isEnabled() method to determine if MockReset is enabled. The logic in the isEnabled() method still relies on the mockitoPresent flag as an initial check; however, mockitoPresent only determines if Mockito is present in the classpath. It does not determine if Mockito can actually be used. For example, it does not detect if the necessary reachability metadata has been registered to use Mockito within a GraalVM native image. To address that last point, the isEnabled() method performs an additional check to determine if Mockito can be used in the current environment. Specifically, it invokes Mockito.mockingDetails().isMock() which in turn initializes core Mockito classes without actually attempting to create a mock. If that fails, that means that Mockito cannot actually be used in the current environment, which typically indicates that GraalVM reachability metadata has not been registered for the org.mockito.plugins.MockMaker in use (such as the ProxyMockMaker). In addition, isEnabled() lazily determines if Mockito can be initialized, since attempting to detect that during static initialization results in a GraalVM native image error stating that Mockito internals were "unintentionally initialized at build time". If Mockito cannot be initialized, MockitoResetTestExecutionListener logs a DEBUG level message providing access to the corresponding stack trace, and MockReset support is disabled. Closes gh-33829
1 parent 0846706 commit ba692aa

File tree

3 files changed

+67
-70
lines changed

3 files changed

+67
-70
lines changed

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java

+50-63
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
package org.springframework.test.context.bean.override.mockito;
1818

19-
import java.lang.reflect.AnnotatedElement;
20-
import java.lang.reflect.Field;
2119
import java.util.Arrays;
2220
import java.util.HashSet;
2321
import java.util.Set;
24-
import java.util.function.Predicate;
2522

23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
2625
import org.mockito.Mockito;
2726

2827
import org.springframework.beans.factory.BeanFactory;
@@ -33,12 +32,8 @@
3332
import org.springframework.context.ApplicationContext;
3433
import org.springframework.context.ConfigurableApplicationContext;
3534
import org.springframework.core.Ordered;
36-
import org.springframework.core.annotation.MergedAnnotation;
37-
import org.springframework.core.annotation.MergedAnnotations;
38-
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3935
import org.springframework.lang.Nullable;
4036
import org.springframework.test.context.TestContext;
41-
import org.springframework.test.context.TestContextAnnotationUtils;
4237
import org.springframework.test.context.support.AbstractTestExecutionListener;
4338
import org.springframework.util.ClassUtils;
4439

@@ -54,13 +49,28 @@
5449
*/
5550
public class MockitoResetTestExecutionListener extends AbstractTestExecutionListener {
5651

52+
private static final Log logger = LogFactory.getLog(MockitoResetTestExecutionListener.class);
53+
54+
/**
55+
* Boolean flag which tracks whether Mockito is present in the classpath.
56+
* @see #mockitoInitialized
57+
* @see #isEnabled()
58+
*/
5759
private static final boolean mockitoPresent = ClassUtils.isPresent("org.mockito.Mockito",
5860
MockitoResetTestExecutionListener.class.getClassLoader());
5961

60-
private static final String SPRING_MOCKITO_PACKAGE = "org.springframework.test.context.bean.override.mockito";
61-
62-
private static final Predicate<MergedAnnotation<?>> isSpringMockitoAnnotation = mergedAnnotation ->
63-
mergedAnnotation.getType().getPackageName().equals(SPRING_MOCKITO_PACKAGE);
62+
/**
63+
* Boolean flag which tracks whether Mockito has been successfully initialized
64+
* in the current environment.
65+
* <p>Even if {@link #mockitoPresent} evaluates to {@code true}, this flag
66+
* may eventually evaluate to {@code false} &mdash; for example, in a GraalVM
67+
* native image if the necessary reachability metadata has not been registered
68+
* for the {@link org.mockito.plugins.MockMaker} in use.
69+
* @see #mockitoPresent
70+
* @see #isEnabled()
71+
*/
72+
@Nullable
73+
private static volatile Boolean mockitoInitialized;
6474

6575

6676
/**
@@ -73,26 +83,26 @@ public int getOrder() {
7383

7484
@Override
7585
public void beforeTestMethod(TestContext testContext) {
76-
if (mockitoPresent && hasMockitoAnnotations(testContext)) {
86+
if (isEnabled()) {
7787
resetMocks(testContext.getApplicationContext(), MockReset.BEFORE);
7888
}
7989
}
8090

8191
@Override
8292
public void afterTestMethod(TestContext testContext) {
83-
if (mockitoPresent && hasMockitoAnnotations(testContext)) {
93+
if (isEnabled()) {
8494
resetMocks(testContext.getApplicationContext(), MockReset.AFTER);
8595
}
8696
}
8797

8898

89-
private void resetMocks(ApplicationContext applicationContext, MockReset reset) {
99+
private static void resetMocks(ApplicationContext applicationContext, MockReset reset) {
90100
if (applicationContext instanceof ConfigurableApplicationContext configurableContext) {
91101
resetMocks(configurableContext, reset);
92102
}
93103
}
94104

95-
private void resetMocks(ConfigurableApplicationContext applicationContext, MockReset reset) {
105+
private static void resetMocks(ConfigurableApplicationContext applicationContext, MockReset reset) {
96106
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
97107
String[] beanNames = beanFactory.getBeanDefinitionNames();
98108
Set<String> instantiatedSingletons = new HashSet<>(Arrays.asList(beanFactory.getSingletonNames()));
@@ -139,59 +149,36 @@ private static boolean isStandardBeanOrSingletonFactoryBean(BeanFactory beanFact
139149
}
140150

141151
/**
142-
* Determine if the test class for the supplied {@linkplain TestContext
143-
* test context} uses any of the annotations in this package (such as
144-
* {@link MockitoBean @MockitoBean}).
145-
*/
146-
static boolean hasMockitoAnnotations(TestContext testContext) {
147-
return hasMockitoAnnotations(testContext.getTestClass());
148-
}
149-
150-
/**
151-
* Determine if Mockito annotations are declared on the supplied class, on an
152-
* interface it implements, on a superclass, or on an enclosing class or
153-
* whether a field in any such class is annotated with a Mockito annotation.
152+
* Determine if this listener is enabled in the current environment.
153+
* @see #mockitoPresent
154+
* @see #mockitoInitialized
154155
*/
155-
private static boolean hasMockitoAnnotations(Class<?> clazz) {
156-
// Declared on the class?
157-
if (isAnnotated(clazz)) {
158-
return true;
159-
}
160-
161-
// Declared on a field?
162-
for (Field field : clazz.getDeclaredFields()) {
163-
if (isAnnotated(field)) {
164-
return true;
165-
}
166-
}
167-
168-
// Declared on an interface?
169-
for (Class<?> ifc : clazz.getInterfaces()) {
170-
if (hasMockitoAnnotations(ifc)) {
171-
return true;
172-
}
156+
private static boolean isEnabled() {
157+
if (!mockitoPresent) {
158+
return false;
173159
}
174-
175-
// Declared on a superclass?
176-
Class<?> superclass = clazz.getSuperclass();
177-
if (superclass != null & superclass != Object.class) {
178-
if (hasMockitoAnnotations(superclass)) {
179-
return true;
160+
Boolean enabled = mockitoInitialized;
161+
if (enabled == null) {
162+
try {
163+
// Invoke isMock() on a non-null object to initialize core Mockito classes
164+
// in order to reliably determine if this listener is "enabled" both on the
165+
// JVM as well as within a GraalVM native image.
166+
Mockito.mockingDetails("a string is not a mock").isMock();
167+
168+
// If we got this far, we assume Mockito is usable in the current environment.
169+
enabled = true;
180170
}
181-
}
182-
183-
// Declared on an enclosing class?
184-
if (TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
185-
if (hasMockitoAnnotations(clazz.getEnclosingClass())) {
186-
return true;
171+
catch (Throwable ex) {
172+
enabled = false;
173+
if (logger.isDebugEnabled()) {
174+
logger.debug("""
175+
MockitoResetTestExecutionListener is disabled in the current environment. \
176+
See exception for details.""", ex);
177+
}
187178
}
179+
mockitoInitialized = enabled;
188180
}
189-
190-
return false;
191-
}
192-
193-
private static boolean isAnnotated(AnnotatedElement element) {
194-
return MergedAnnotations.from(element, SearchStrategy.DIRECT).stream().anyMatch(isSpringMockitoAnnotation);
181+
return enabled;
195182
}
196183

197184
}

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithMockitoBeanIntegrationTests.java

+17-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.mockito.BDDMockito.given;
23+
2124
/**
2225
* Integration tests for {@link MockitoResetTestExecutionListener} with a
2326
* {@link MockitoBean @MockitoBean} field.
@@ -29,15 +32,24 @@
2932
class MockitoResetTestExecutionListenerWithMockitoBeanIntegrationTests
3033
extends MockitoResetTestExecutionListenerWithoutMockitoAnnotationsIntegrationTests {
3134

32-
// The following mock is not used but is currently required to trigger support for MockReset.
35+
// We declare the following to ensure that MockReset is also supported with
36+
// @MockitoBean or @MockitoSpyBean fields present in the test class.
3337
@MockitoBean
34-
StringBuilder unusedVariable;
38+
PuzzleService puzzleService;
39+
3540

41+
// test001() and test002() are in the superclass.
3642

3743
@Test
38-
@Override
39-
void test002() {
40-
super.test002();
44+
void test003() {
45+
given(puzzleService.getAnswer()).willReturn("enigma");
46+
assertThat(puzzleService.getAnswer()).isEqualTo("enigma");
47+
}
48+
49+
50+
interface PuzzleService {
51+
52+
String getAnswer();
4153
}
4254

4355
}

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithoutMockitoAnnotationsIntegrationTests.java

-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.test.context.bean.override.mockito;
1818

19-
import org.junit.jupiter.api.Disabled;
2019
import org.junit.jupiter.api.MethodOrderer;
2120
import org.junit.jupiter.api.Test;
2221
import org.junit.jupiter.api.TestMethodOrder;
@@ -76,7 +75,6 @@ void test001() {
7675
assertThat(context.getBean(NonSingletonFactoryBean.class).getObjectInvocations).isEqualTo(2);
7776
}
7877

79-
@Disabled("MockReset is currently only honored if @MockitoBean or @MockitoSpyBean is used")
8078
@Test
8179
void test002() {
8280
// Should not have been reset.

0 commit comments

Comments
 (0)