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:
Simon Baslé
2024-08-07 14:57:15 +02:00
parent bc05474271
commit 1c893e6354
5 changed files with 239 additions and 13 deletions

View File

@@ -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();
}

View File

@@ -21,9 +21,12 @@ import java.lang.reflect.Field;
import java.util.LinkedHashSet;
import java.util.Set;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.mockito.BDDMockito;
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.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@@ -32,10 +35,14 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
/**
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} and
* {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers
* {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are
* used, primarily to support {@link Captor @Captor} annotations.
* {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean}
* and {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers Mockito set
* up of a {@link Mockito#mockitoSession() session} for each test class that
* 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
* handled by the {@link MockitoResetTestExecutionListener}.
@@ -97,37 +104,67 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener
private void initMocks(TestContext testContext) {
if (hasMockitoAnnotations(testContext)) {
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 {
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();
}
}
private boolean hasMockitoAnnotations(TestContext testContext) {
MockitoAnnotationCollector collector = new MockitoAnnotationCollector();
ReflectionUtils.doWithFields(testContext.getTestClass(), collector);
collector.collect(testContext.getTestClass());
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 String MOCKITO_BEAN_PACKAGE = MockitoBean.class.getPackageName();
private static final String ORG_MOCKITO_PACKAGE = "org.mockito";
private final Set<Annotation> annotations = new LinkedHashSet<>();
public void collect(Class<?> clazz) {
ReflectionUtils.doWithFields(clazz, this);
for (Annotation annotation : clazz.getAnnotations()) {
collect(annotation);
}
}
@Override
public void doWith(Field field) throws IllegalArgumentException {
for (Annotation annotation : field.getAnnotations()) {
if (annotation.annotationType().getPackageName().startsWith("org.mockito")) {
this.annotations.add(annotation);
}
collect(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);
}
}