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
This commit is contained in:
parent
bc05474271
commit
1c893e6354
|
@ -23,6 +23,9 @@ creating unnecessary contexts.
|
||||||
====
|
====
|
||||||
|
|
||||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
|
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
|
||||||
|
During the test class lifecycle, Mockito is set up via the `Mockito#mockitoSession()`
|
||||||
|
mechanism. Notably, it enables `STRICT_STUBS` mode by default. This can be changed on
|
||||||
|
individual test classes with the `@MockitoBeanSettings` annotation.
|
||||||
|
|
||||||
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
|
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
|
||||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
|
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.test.context.bean.override.mockito;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a test class that uses {@link MockitoBean} or {@link MockitoSpyBean}
|
||||||
|
* to set up Mockito with an explicitly specified stubbing strictness.
|
||||||
|
*
|
||||||
|
* @author Simon Baslé
|
||||||
|
* @since 6.2
|
||||||
|
* @see MockitoTestExecutionListener
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface MockitoBeanSettings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stubbing strictness to apply for all Mockito mocks in the annotated
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
Strictness value();
|
||||||
|
|
||||||
|
}
|
|
@ -21,9 +21,12 @@ import java.lang.reflect.Field;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.mockito.Captor;
|
import org.mockito.BDDMockito;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.MockitoSession;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.test.context.TestContext;
|
import org.springframework.test.context.TestContext;
|
||||||
import org.springframework.test.context.support.AbstractTestExecutionListener;
|
import org.springframework.test.context.support.AbstractTestExecutionListener;
|
||||||
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
||||||
|
@ -32,10 +35,14 @@ import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.ReflectionUtils.FieldCallback;
|
import org.springframework.util.ReflectionUtils.FieldCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} and
|
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean}
|
||||||
* {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers
|
* and {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers Mockito set
|
||||||
* {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are
|
* up of a {@link Mockito#mockitoSession() session} for each test class that
|
||||||
* used, primarily to support {@link Captor @Captor} annotations.
|
* uses these annotations (or any annotation in that package).
|
||||||
|
*
|
||||||
|
* <p>The {@link MockitoSession#setStrictness(Strictness) strictness} of the
|
||||||
|
* session defaults to {@link Strictness#STRICT_STUBS}. Use
|
||||||
|
* {@link MockitoBeanSettings} to specify a different strictness.
|
||||||
*
|
*
|
||||||
* <p>The automatic reset support for {@code @MockBean} and {@code @SpyBean} is
|
* <p>The automatic reset support for {@code @MockBean} and {@code @SpyBean} is
|
||||||
* handled by the {@link MockitoResetTestExecutionListener}.
|
* handled by the {@link MockitoResetTestExecutionListener}.
|
||||||
|
@ -97,37 +104,67 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener
|
||||||
private void initMocks(TestContext testContext) {
|
private void initMocks(TestContext testContext) {
|
||||||
if (hasMockitoAnnotations(testContext)) {
|
if (hasMockitoAnnotations(testContext)) {
|
||||||
Object testInstance = testContext.getTestInstance();
|
Object testInstance = testContext.getTestInstance();
|
||||||
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testInstance));
|
MockitoBeanSettings annotation = AnnotationUtils.findAnnotation(testInstance.getClass(),
|
||||||
|
MockitoBeanSettings.class);
|
||||||
|
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, initMockitoSession(testInstance,
|
||||||
|
annotation == null ? Strictness.STRICT_STUBS: annotation.value()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MockitoSession initMockitoSession(Object testInstance, Strictness strictness) {
|
||||||
|
return BDDMockito.mockitoSession()
|
||||||
|
.initMocks(testInstance)
|
||||||
|
.strictness(strictness)
|
||||||
|
.startMocking();
|
||||||
|
}
|
||||||
|
|
||||||
private void closeMocks(TestContext testContext) throws Exception {
|
private void closeMocks(TestContext testContext) throws Exception {
|
||||||
Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME);
|
Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME);
|
||||||
if (mocks instanceof AutoCloseable closeable) {
|
if (mocks instanceof MockitoSession session) {
|
||||||
|
session.finishMocking();
|
||||||
|
}
|
||||||
|
else if (mocks instanceof AutoCloseable closeable) {
|
||||||
closeable.close();
|
closeable.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasMockitoAnnotations(TestContext testContext) {
|
private boolean hasMockitoAnnotations(TestContext testContext) {
|
||||||
MockitoAnnotationCollector collector = new MockitoAnnotationCollector();
|
MockitoAnnotationCollector collector = new MockitoAnnotationCollector();
|
||||||
ReflectionUtils.doWithFields(testContext.getTestClass(), collector);
|
collector.collect(testContext.getTestClass());
|
||||||
return collector.hasAnnotations();
|
return collector.hasAnnotations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link FieldCallback} that collects Mockito annotations.
|
* Utility class that collects {@code org.mockito} annotations and the
|
||||||
|
* annotations in this package (like {@link MockitoBeanSettings}).
|
||||||
*/
|
*/
|
||||||
private static final class MockitoAnnotationCollector implements FieldCallback {
|
private static final class MockitoAnnotationCollector implements FieldCallback {
|
||||||
|
|
||||||
|
private static final String MOCKITO_BEAN_PACKAGE = MockitoBean.class.getPackageName();
|
||||||
|
private static final String ORG_MOCKITO_PACKAGE = "org.mockito";
|
||||||
|
|
||||||
private final Set<Annotation> annotations = new LinkedHashSet<>();
|
private final Set<Annotation> annotations = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
public void collect(Class<?> clazz) {
|
||||||
|
ReflectionUtils.doWithFields(clazz, this);
|
||||||
|
for (Annotation annotation : clazz.getAnnotations()) {
|
||||||
|
collect(annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doWith(Field field) throws IllegalArgumentException {
|
public void doWith(Field field) throws IllegalArgumentException {
|
||||||
for (Annotation annotation : field.getAnnotations()) {
|
for (Annotation annotation : field.getAnnotations()) {
|
||||||
if (annotation.annotationType().getPackageName().startsWith("org.mockito")) {
|
collect(annotation);
|
||||||
this.annotations.add(annotation);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void collect(Annotation annotation) {
|
||||||
|
String packageName = annotation.annotationType().getPackageName();
|
||||||
|
if (packageName.startsWith(MOCKITO_BEAN_PACKAGE) ||
|
||||||
|
packageName.startsWith(ORG_MOCKITO_PACKAGE)) {
|
||||||
|
this.annotations.add(annotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.test.context.bean.override.mockito;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for explicitly-defined {@link MockitoBeanSettings} with
|
||||||
|
* lenient stubbing.
|
||||||
|
*
|
||||||
|
* @author Simon Baslé
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
@SpringJUnitConfig(MockitoBeanForByNameLookupIntegrationTests.Config.class)
|
||||||
|
@MockitoBeanSettings(Strictness.LENIENT)
|
||||||
|
public class MockitoBeanSettingsLenientIntegrationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unusedStubbingNotReported() {
|
||||||
|
var list = Mockito.mock(List.class);
|
||||||
|
Mockito.when(list.get(Mockito.anyInt())).thenReturn(new Object());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.test.context.bean.override.mockito;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Named;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||||
|
import org.junit.platform.testkit.engine.Events;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBeanForByNameLookupIntegrationTests.Config;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
|
|
||||||
|
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||||
|
import static org.junit.platform.testkit.engine.EventConditions.event;
|
||||||
|
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
|
||||||
|
import static org.junit.platform.testkit.engine.EventConditions.test;
|
||||||
|
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
|
||||||
|
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests ensuring unnecessary stubbings are reported in various
|
||||||
|
* cases where a strict style is chosen or assumed.
|
||||||
|
*
|
||||||
|
* @author Simon Baslé
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
public final class MockitoBeanSettingsStrictIntegrationTests {
|
||||||
|
|
||||||
|
private static Stream<Named<Class<?>>> strictCases() {
|
||||||
|
return Stream.of(
|
||||||
|
Named.of("explicit strictness", ExplicitStrictness.class),
|
||||||
|
Named.of("implicit strictness with @MockitoBean on field", ImplicitStrictnessWithMockitoBean.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("strictCases")
|
||||||
|
public void unusedStubbingIsReported(Class<?> forCase) {
|
||||||
|
Events events = EngineTestKit.engine("junit-jupiter")
|
||||||
|
.selectors(selectClass(forCase))
|
||||||
|
.execute()
|
||||||
|
.testEvents()
|
||||||
|
.assertStatistics(stats -> stats.started(1).failed(1));
|
||||||
|
|
||||||
|
events.assertThatEvents().haveExactly(1,
|
||||||
|
event(test("unnecessaryStub"),
|
||||||
|
finishedWithFailure(
|
||||||
|
instanceOf(UnnecessaryStubbingException.class),
|
||||||
|
message(msg -> msg.contains("Unnecessary stubbings detected.")))));
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract static class BaseCase {
|
||||||
|
@Test
|
||||||
|
void unnecessaryStub() {
|
||||||
|
var list = Mockito.mock(List.class);
|
||||||
|
Mockito.when(list.get(Mockito.anyInt())).thenReturn(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SpringJUnitConfig(Config.class)
|
||||||
|
@MockitoBeanSettings(Strictness.STRICT_STUBS)
|
||||||
|
static class ExplicitStrictness extends BaseCase {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SpringJUnitConfig(Config.class)
|
||||||
|
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
DateTimeFormatter ignoredMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue