Skip to content

Commit eb4bf1c

Browse files
committed
Support @⁠MockitoBean reset and MockitoSession management with @⁠Nested tests
Prior to this commit, the MockitoResetTestExecutionListener failed to reset mocks created via @⁠MockitoBean if the @⁠MockitoBean field was declared in an enclosing class for a @⁠Nested test class. In addition, the MockitoSession was not properly managed by the MockitoTestExecutionListener. This commit addresses those issue as follows. 1) The hasMockitoAnnotations() utility method has been overhauled so that it finds Mockito annotations not only on the current test class and on fields of the current test class but also on interfaces, superclasses, and enclosing classes for @⁠Nested test classes as well as on fields of superclasses and enclosing classes. That allows the MockitoResetTestExecutionListener to properly detect that it needs to reset mocks for fields declared in enclosing classes for @⁠Nested classes. 2) MockitoTestExecutionListener has been revised so that it only initializes a MockitoSession before each test method and closes the MockitoSession after each test method. In addition, it now only manages the MockitoSession when hasMockitoAnnotations() returns true for the current test class (which may be a @⁠Nested test class). Furthermore, it no longer attempts to initialize a MockitoSession during the prepareTestInstance() callback since that results in an UnfinishedMockingSessionException for a @⁠Nested test class due to the fact that a MockitoSession was already created for the current thread for the enclosing test class. Closes gh-33676
1 parent 7fb6a2e commit eb4bf1c

File tree

3 files changed

+151
-49
lines changed

3 files changed

+151
-49
lines changed

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

+45-23
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616

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

19-
import java.lang.annotation.Annotation;
20-
import java.lang.reflect.AnnotatedElement;
21-
import java.util.Arrays;
22-
import java.util.concurrent.atomic.AtomicBoolean;
19+
import java.lang.reflect.Field;
2320
import java.util.function.Predicate;
2421

22+
import org.springframework.core.annotation.MergedAnnotation;
23+
import org.springframework.core.annotation.MergedAnnotations;
24+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
2525
import org.springframework.test.context.TestContext;
26+
import org.springframework.test.context.TestContextAnnotationUtils;
2627
import org.springframework.test.context.support.AbstractTestExecutionListener;
2728
import org.springframework.util.ClassUtils;
28-
import org.springframework.util.ReflectionUtils;
2929

3030
/**
3131
* Abstract base class for {@code TestExecutionListener} implementations involving
@@ -44,8 +44,8 @@ abstract class AbstractMockitoTestExecutionListener extends AbstractTestExecutio
4444

4545
private static final String ORG_MOCKITO_PACKAGE = "org.mockito";
4646

47-
private static final Predicate<Annotation> isMockitoAnnotation = annotation -> {
48-
String packageName = annotation.annotationType().getPackageName();
47+
private static final Predicate<MergedAnnotation<?>> isMockitoAnnotation = mergedAnnotation -> {
48+
String packageName = mergedAnnotation.getType().getPackageName();
4949
return (packageName.startsWith(SPRING_MOCKITO_PACKAGE) ||
5050
packageName.startsWith(ORG_MOCKITO_PACKAGE));
5151
};
@@ -60,25 +60,47 @@ static boolean hasMockitoAnnotations(TestContext testContext) {
6060
return hasMockitoAnnotations(testContext.getTestClass());
6161
}
6262

63-
private static boolean hasMockitoAnnotations(Class<?> testClass) {
64-
if (isAnnotated(testClass)) {
63+
/**
64+
* Determine if Mockito annotations are declared on the supplied class, on an
65+
* interface it implements, on a superclass, or on an enclosing class or
66+
* whether a field in any such class is annotated with a Mockito annotation.
67+
*/
68+
private static boolean hasMockitoAnnotations(Class<?> clazz) {
69+
// Declared on the class?
70+
if (MergedAnnotations.from(clazz, SearchStrategy.DIRECT).stream().anyMatch(isMockitoAnnotation)) {
6571
return true;
6672
}
67-
// TODO Ideally we should short-circuit the search once we've found a Mockito annotation,
68-
// since there's no need to continue searching additional fields or further up the class
69-
// hierarchy; however, that is not possible with ReflectionUtils#doWithFields. Plus, the
70-
// previous invocation of isAnnotated(testClass) only finds annotations declared directly
71-
// on the test class. So, we'll likely need a completely different approach that combines
72-
// the "test class/interface is annotated?" and "field is annotated?" checks in a single
73-
// search algorithm, and we'll also need to support @Nested class hierarchies.
74-
AtomicBoolean found = new AtomicBoolean();
75-
ReflectionUtils.doWithFields(testClass,
76-
field -> found.set(true), AbstractMockitoTestExecutionListener::isAnnotated);
77-
return found.get();
78-
}
7973

80-
private static boolean isAnnotated(AnnotatedElement annotatedElement) {
81-
return Arrays.stream(annotatedElement.getAnnotations()).anyMatch(isMockitoAnnotation);
74+
// Declared on a field?
75+
for (Field field : clazz.getDeclaredFields()) {
76+
if (MergedAnnotations.from(field, SearchStrategy.DIRECT).stream().anyMatch(isMockitoAnnotation)) {
77+
return true;
78+
}
79+
}
80+
81+
// Declared on an interface?
82+
for (Class<?> ifc : clazz.getInterfaces()) {
83+
if (hasMockitoAnnotations(ifc)) {
84+
return true;
85+
}
86+
}
87+
88+
// Declared on a superclass?
89+
Class<?> superclass = clazz.getSuperclass();
90+
if (superclass != null & superclass != Object.class) {
91+
if (hasMockitoAnnotations(superclass)) {
92+
return true;
93+
}
94+
}
95+
96+
// Declared on an enclosing class of an inner class?
97+
if (TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
98+
if (hasMockitoAnnotations(clazz.getEnclosingClass())) {
99+
return true;
100+
}
101+
}
102+
103+
return false;
82104
}
83105

84106
}

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

+7-26
Original file line numberDiff line numberDiff line change
@@ -63,45 +63,26 @@ public final int getOrder() {
6363
return 1950;
6464
}
6565

66-
@Override
67-
public void prepareTestInstance(TestContext testContext) {
68-
if (mockitoPresent) {
69-
closeMocks(testContext);
70-
initMocks(testContext);
71-
}
72-
}
73-
7466
@Override
7567
public void beforeTestMethod(TestContext testContext) {
76-
if (mockitoPresent && Boolean.TRUE.equals(
77-
testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) {
78-
closeMocks(testContext);
68+
if (mockitoPresent && hasMockitoAnnotations(testContext)) {
7969
initMocks(testContext);
8070
}
8171
}
8272

8373
@Override
8474
public void afterTestMethod(TestContext testContext) {
85-
if (mockitoPresent) {
86-
closeMocks(testContext);
87-
}
88-
}
89-
90-
@Override
91-
public void afterTestClass(TestContext testContext) {
92-
if (mockitoPresent) {
75+
if (mockitoPresent && hasMockitoAnnotations(testContext)) {
9376
closeMocks(testContext);
9477
}
9578
}
9679

9780
private static void initMocks(TestContext testContext) {
98-
if (hasMockitoAnnotations(testContext)) {
99-
Class<?> testClass = testContext.getTestClass();
100-
Object testInstance = testContext.getTestInstance();
101-
MockitoBeanSettings annotation = AnnotationUtils.findAnnotation(testClass, MockitoBeanSettings.class);
102-
Strictness strictness = (annotation != null ? annotation.value() : Strictness.STRICT_STUBS);
103-
testContext.setAttribute(MOCKITO_SESSION_ATTRIBUTE_NAME, initMockitoSession(testInstance, strictness));
104-
}
81+
Class<?> testClass = testContext.getTestClass();
82+
Object testInstance = testContext.getTestInstance();
83+
MockitoBeanSettings annotation = AnnotationUtils.findAnnotation(testClass, MockitoBeanSettings.class);
84+
Strictness strictness = (annotation != null ? annotation.value() : Strictness.STRICT_STUBS);
85+
testContext.setAttribute(MOCKITO_SESSION_ATTRIBUTE_NAME, initMockitoSession(testInstance, strictness));
10586
}
10687

10788
private static MockitoSession initMockitoSession(Object testInstance, Strictness strictness) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.ExtendWith;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.test.context.ContextConfiguration;
27+
import org.springframework.test.context.junit.jupiter.SpringExtension;
28+
29+
import static org.mockito.BDDMockito.then;
30+
import static org.mockito.Mockito.times;
31+
32+
/**
33+
* Verifies proper handling of the {@link org.mockito.MockitoSession MockitoSession}
34+
* when a {@link MockitoBean @MockitoBean} field is declared in the enclosing class of
35+
* a {@link Nested @Nested} test class.
36+
*
37+
* @author Andy Wilkinson
38+
* @author Sam Brannen
39+
* @since 6.2
40+
*/
41+
@ExtendWith(SpringExtension.class)
42+
// TODO Remove @ContextConfiguration declaration.
43+
// @ContextConfiguration is currently required due to a bug in the TestContext framework.
44+
@ContextConfiguration
45+
class MockitoBeanNestedTests {
46+
47+
@MockitoBean
48+
Runnable action;
49+
50+
@Autowired
51+
Task task;
52+
53+
@Test
54+
void mockWasInvokedOnce() {
55+
task.execute();
56+
then(action).should().run();
57+
}
58+
59+
@Test
60+
void mockWasInvokedTwice() {
61+
task.execute();
62+
task.execute();
63+
then(action).should(times(2)).run();
64+
}
65+
66+
@Nested
67+
class MockitoBeanFieldInEnclosingClassTests {
68+
69+
@Test
70+
void mockWasInvokedOnce() {
71+
task.execute();
72+
then(action).should().run();
73+
}
74+
75+
@Test
76+
void mockWasInvokedTwice() {
77+
task.execute();
78+
task.execute();
79+
then(action).should(times(2)).run();
80+
}
81+
}
82+
83+
record Task(Runnable action) {
84+
85+
void execute() {
86+
this.action.run();
87+
}
88+
}
89+
90+
@Configuration(proxyBeanMethods = false)
91+
static class TestConfiguration {
92+
93+
@Bean
94+
Task task(Runnable action) {
95+
return new Task(action);
96+
}
97+
}
98+
99+
}

0 commit comments

Comments
 (0)