Skip to content

Commit 1c893e6

Browse files
committed
Add @MockitoBeanSettings, use MockitoSession with strict stubs default
This commit changes the way the `MockitoTestExecutionListener` sets up mockito, now using the `MockitoSession` feature. Additionally, stubbing now defaults to a STRICT mode which can be overruled with a newly introduced annotation: `@MockitoBeanSettings`. Closes gh-33318
1 parent bc05474 commit 1c893e6

File tree

5 files changed

+239
-13
lines changed

5 files changed

+239
-13
lines changed

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ creating unnecessary contexts.
2323
====
2424

2525
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
26+
During the test class lifecycle, Mockito is set up via the `Mockito#mockitoSession()`
27+
mechanism. Notably, it enables `STRICT_STUBS` mode by default. This can be changed on
28+
individual test classes with the `@MockitoBeanSettings` annotation.
2629

2730
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
2831
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.mockito.quality.Strictness;
26+
27+
/**
28+
* Configure a test class that uses {@link MockitoBean} or {@link MockitoSpyBean}
29+
* to set up Mockito with an explicitly specified stubbing strictness.
30+
*
31+
* @author Simon Baslé
32+
* @since 6.2
33+
* @see MockitoTestExecutionListener
34+
*/
35+
@Target(ElementType.TYPE)
36+
@Retention(RetentionPolicy.RUNTIME)
37+
@Documented
38+
public @interface MockitoBeanSettings {
39+
40+
/**
41+
* The stubbing strictness to apply for all Mockito mocks in the annotated
42+
* class.
43+
*/
44+
Strictness value();
45+
46+
}

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

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.Set;
2323

24-
import org.mockito.Captor;
25-
import org.mockito.MockitoAnnotations;
24+
import org.mockito.BDDMockito;
25+
import org.mockito.Mockito;
26+
import org.mockito.MockitoSession;
27+
import org.mockito.quality.Strictness;
2628

29+
import org.springframework.core.annotation.AnnotationUtils;
2730
import org.springframework.test.context.TestContext;
2831
import org.springframework.test.context.support.AbstractTestExecutionListener;
2932
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@@ -32,10 +35,14 @@
3235
import org.springframework.util.ReflectionUtils.FieldCallback;
3336

3437
/**
35-
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} and
36-
* {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers
37-
* {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are
38-
* used, primarily to support {@link Captor @Captor} annotations.
38+
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean}
39+
* and {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers Mockito set
40+
* up of a {@link Mockito#mockitoSession() session} for each test class that
41+
* uses these annotations (or any annotation in that package).
42+
*
43+
* <p>The {@link MockitoSession#setStrictness(Strictness) strictness} of the
44+
* session defaults to {@link Strictness#STRICT_STUBS}. Use
45+
* {@link MockitoBeanSettings} to specify a different strictness.
3946
*
4047
* <p>The automatic reset support for {@code @MockBean} and {@code @SpyBean} is
4148
* handled by the {@link MockitoResetTestExecutionListener}.
@@ -97,37 +104,67 @@ public void afterTestClass(TestContext testContext) throws Exception {
97104
private void initMocks(TestContext testContext) {
98105
if (hasMockitoAnnotations(testContext)) {
99106
Object testInstance = testContext.getTestInstance();
100-
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testInstance));
107+
MockitoBeanSettings annotation = AnnotationUtils.findAnnotation(testInstance.getClass(),
108+
MockitoBeanSettings.class);
109+
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, initMockitoSession(testInstance,
110+
annotation == null ? Strictness.STRICT_STUBS: annotation.value()));
101111
}
102112
}
103113

114+
private MockitoSession initMockitoSession(Object testInstance, Strictness strictness) {
115+
return BDDMockito.mockitoSession()
116+
.initMocks(testInstance)
117+
.strictness(strictness)
118+
.startMocking();
119+
}
120+
104121
private void closeMocks(TestContext testContext) throws Exception {
105122
Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME);
106-
if (mocks instanceof AutoCloseable closeable) {
123+
if (mocks instanceof MockitoSession session) {
124+
session.finishMocking();
125+
}
126+
else if (mocks instanceof AutoCloseable closeable) {
107127
closeable.close();
108128
}
109129
}
110130

111131
private boolean hasMockitoAnnotations(TestContext testContext) {
112132
MockitoAnnotationCollector collector = new MockitoAnnotationCollector();
113-
ReflectionUtils.doWithFields(testContext.getTestClass(), collector);
133+
collector.collect(testContext.getTestClass());
114134
return collector.hasAnnotations();
115135
}
116136

117137

118138
/**
119-
* {@link FieldCallback} that collects Mockito annotations.
139+
* Utility class that collects {@code org.mockito} annotations and the
140+
* annotations in this package (like {@link MockitoBeanSettings}).
120141
*/
121142
private static final class MockitoAnnotationCollector implements FieldCallback {
122143

144+
private static final String MOCKITO_BEAN_PACKAGE = MockitoBean.class.getPackageName();
145+
private static final String ORG_MOCKITO_PACKAGE = "org.mockito";
146+
123147
private final Set<Annotation> annotations = new LinkedHashSet<>();
124148

149+
public void collect(Class<?> clazz) {
150+
ReflectionUtils.doWithFields(clazz, this);
151+
for (Annotation annotation : clazz.getAnnotations()) {
152+
collect(annotation);
153+
}
154+
}
155+
125156
@Override
126157
public void doWith(Field field) throws IllegalArgumentException {
127158
for (Annotation annotation : field.getAnnotations()) {
128-
if (annotation.annotationType().getPackageName().startsWith("org.mockito")) {
129-
this.annotations.add(annotation);
130-
}
159+
collect(annotation);
160+
}
161+
}
162+
163+
private void collect(Annotation annotation) {
164+
String packageName = annotation.annotationType().getPackageName();
165+
if (packageName.startsWith(MOCKITO_BEAN_PACKAGE) ||
166+
packageName.startsWith(ORG_MOCKITO_PACKAGE)) {
167+
this.annotations.add(annotation);
131168
}
132169
}
133170

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.Mockito;
23+
import org.mockito.quality.Strictness;
24+
25+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
26+
27+
/**
28+
* Integration tests for explicitly-defined {@link MockitoBeanSettings} with
29+
* lenient stubbing.
30+
*
31+
* @author Simon Baslé
32+
* @since 6.2
33+
*/
34+
@SpringJUnitConfig(MockitoBeanForByNameLookupIntegrationTests.Config.class)
35+
@MockitoBeanSettings(Strictness.LENIENT)
36+
public class MockitoBeanSettingsLenientIntegrationTests {
37+
38+
@Test
39+
public void unusedStubbingNotReported() {
40+
var list = Mockito.mock(List.class);
41+
Mockito.when(list.get(Mockito.anyInt())).thenReturn(new Object());
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 java.time.format.DateTimeFormatter;
20+
import java.util.List;
21+
import java.util.stream.Stream;
22+
23+
import org.junit.jupiter.api.Named;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.MethodSource;
27+
import org.junit.platform.testkit.engine.EngineTestKit;
28+
import org.junit.platform.testkit.engine.Events;
29+
import org.mockito.Mockito;
30+
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
31+
import org.mockito.quality.Strictness;
32+
33+
import org.springframework.test.context.bean.override.mockito.MockitoBeanForByNameLookupIntegrationTests.Config;
34+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
35+
36+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
37+
import static org.junit.platform.testkit.engine.EventConditions.event;
38+
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
39+
import static org.junit.platform.testkit.engine.EventConditions.test;
40+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
41+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
42+
43+
/**
44+
* Integration tests ensuring unnecessary stubbings are reported in various
45+
* cases where a strict style is chosen or assumed.
46+
*
47+
* @author Simon Baslé
48+
* @since 6.2
49+
*/
50+
public final class MockitoBeanSettingsStrictIntegrationTests {
51+
52+
private static Stream<Named<Class<?>>> strictCases() {
53+
return Stream.of(
54+
Named.of("explicit strictness", ExplicitStrictness.class),
55+
Named.of("implicit strictness with @MockitoBean on field", ImplicitStrictnessWithMockitoBean.class)
56+
);
57+
}
58+
59+
@ParameterizedTest
60+
@MethodSource("strictCases")
61+
public void unusedStubbingIsReported(Class<?> forCase) {
62+
Events events = EngineTestKit.engine("junit-jupiter")
63+
.selectors(selectClass(forCase))
64+
.execute()
65+
.testEvents()
66+
.assertStatistics(stats -> stats.started(1).failed(1));
67+
68+
events.assertThatEvents().haveExactly(1,
69+
event(test("unnecessaryStub"),
70+
finishedWithFailure(
71+
instanceOf(UnnecessaryStubbingException.class),
72+
message(msg -> msg.contains("Unnecessary stubbings detected.")))));
73+
}
74+
75+
abstract static class BaseCase {
76+
@Test
77+
void unnecessaryStub() {
78+
var list = Mockito.mock(List.class);
79+
Mockito.when(list.get(Mockito.anyInt())).thenReturn(new Object());
80+
}
81+
}
82+
83+
@SpringJUnitConfig(Config.class)
84+
@MockitoBeanSettings(Strictness.STRICT_STUBS)
85+
static class ExplicitStrictness extends BaseCase {
86+
}
87+
88+
@SpringJUnitConfig(Config.class)
89+
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
90+
91+
@MockitoBean
92+
@SuppressWarnings("unused")
93+
DateTimeFormatter ignoredMock;
94+
}
95+
96+
}

0 commit comments

Comments
 (0)