From 1b6b9efcb24ddab897f2dc18a3dc0c7cfd5093c4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 16 Oct 2024 10:22:31 +0100 Subject: [PATCH] Reinstate init of Mockito mocks in test execution listener Closes gh-42708 --- .../mockito/MockitoTestExecutionListener.java | 74 ++++++++++++++++++- ...InheritedNestedTestConfigurationTests.java | 2 - ...TestExecutionListenerIntegrationTests.java | 1 - .../MockitoTestExecutionListenerTests.java | 8 ++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java index 56d9568457..941b878403 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java @@ -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. *

* 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 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; diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java index 51d6077f52..1cc5c44492 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java @@ -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 diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerIntegrationTests.java index aaf336e5c0..fea91da20a 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerIntegrationTests.java @@ -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 diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java index 95b7e02b84..09dec0c271 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java @@ -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);