Reinstate init of Mockito mocks in test execution listener

Closes gh-42708
This commit is contained in:
Andy Wilkinson 2024-10-16 10:22:31 +01:00
parent 2014176d46
commit 1b6b9efcb2
4 changed files with 79 additions and 6 deletions

View File

@ -16,18 +16,29 @@
package org.springframework.boot.test.mock.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.BiConsumer;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
/**
* {@link TestExecutionListener} to enable {@link MockBean @MockBean} and
* {@link SpyBean @SpyBean} support.
* {@link SpyBean @SpyBean} support. Also triggers
* {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations used,
* primarily to allow {@link Captor @Captor} annotations.
* <p>
* To use the automatic reset support of {@code @MockBean} and {@code @SpyBean}, configure
* {@link ResetMocksTestExecutionListener} as well.
@ -37,13 +48,15 @@ import org.springframework.util.ReflectionUtils;
* @author Moritz Halbritter
* @since 1.4.2
* @see ResetMocksTestExecutionListener
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
* {@link org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener}
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of Spring Framework's support for
* {@link MockitoBean} and {@link MockitoSpyBean}.
*/
@SuppressWarnings("removal")
@Deprecated(since = "3.4.0", forRemoval = true)
public class MockitoTestExecutionListener extends AbstractTestExecutionListener {
private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks";
@Override
public final int getOrder() {
return 1950;
@ -51,6 +64,8 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
closeMocks(testContext);
initMocks(testContext);
injectFields(testContext);
}
@ -58,10 +73,41 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener
public void beforeTestMethod(TestContext testContext) throws Exception {
if (Boolean.TRUE.equals(
testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) {
closeMocks(testContext);
initMocks(testContext);
reinjectFields(testContext);
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
closeMocks(testContext);
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
closeMocks(testContext);
}
private void initMocks(TestContext testContext) {
if (hasMockitoAnnotations(testContext)) {
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testContext.getTestInstance()));
}
}
private void closeMocks(TestContext testContext) throws Exception {
Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME);
if (mocks instanceof AutoCloseable closeable) {
closeable.close();
}
}
private boolean hasMockitoAnnotations(TestContext testContext) {
MockitoAnnotationCollection collector = new MockitoAnnotationCollection();
ReflectionUtils.doWithFields(testContext.getTestClass(), collector);
return collector.hasAnnotations();
}
private void injectFields(TestContext testContext) {
postProcessFields(testContext, (mockitoField, postProcessor) -> postProcessor.inject(mockitoField.field,
mockitoField.target, mockitoField.definition));
@ -90,6 +136,28 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener
}
}
/**
* {@link FieldCallback} to collect Mockito annotations.
*/
private static final class MockitoAnnotationCollection implements FieldCallback {
private final Set<Annotation> annotations = new LinkedHashSet<>();
@Override
public void doWith(Field field) throws IllegalArgumentException {
for (Annotation annotation : field.getDeclaredAnnotations()) {
if (annotation.annotationType().getName().startsWith("org.mockito")) {
this.annotations.add(annotation);
}
}
}
boolean hasAnnotations() {
return !this.annotations.isEmpty();
}
}
private static final class MockitoField {
private final Field field;

View File

@ -16,7 +16,6 @@
package org.springframework.boot.test.context.nestedtests;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@ -40,7 +39,6 @@ import static org.mockito.Mockito.times;
*/
@SpringBootTest(classes = AppConfiguration.class)
@Import(ActionPerformer.class)
@Disabled("https://github.com/spring-projects/spring-framework/issues/33676")
class InheritedNestedTestConfigurationTests {
@MockitoBean

View File

@ -54,7 +54,6 @@ import static org.mockito.BDDMockito.given;
@SuppressWarnings("removal")
@Deprecated(since = "3.4.0", forRemoval = true)
@ExtendWith(SpringExtension.class)
@Disabled("https://github.com/spring-projects/spring-framework/issues/33690")
class MockitoTestExecutionListenerIntegrationTests {
@Nested

View File

@ -56,6 +56,14 @@ class MockitoTestExecutionListenerTests {
@Mock
private MockitoPostProcessor postProcessor;
@Test
void prepareTestInstanceShouldInitMockitoAnnotations() throws Exception {
WithMockitoAnnotations instance = new WithMockitoAnnotations();
this.listener.prepareTestInstance(mockTestContext(instance));
assertThat(instance.mock).isNotNull();
assertThat(instance.captor).isNotNull();
}
@Test
void prepareTestInstanceShouldInjectMockBean() throws Exception {
given(this.applicationContext.getBean(MockitoPostProcessor.class)).willReturn(this.postProcessor);