diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 02147a3992e..52bd5f3fdcf 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -92,7 +92,7 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList @Override public void beforeTestMethod(TestContext testContext) { - if (isEnabled()) { + if (isEnabled() && testContext.hasApplicationContext()) { resetMocks(testContext.getApplicationContext(), MockReset.BEFORE); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerTests.java new file mode 100644 index 00000000000..1b11261f4cd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-present 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 org.junit.jupiter.api.Test; + +import org.springframework.test.context.TestContext; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link MockitoResetTestExecutionListener}. + * + * @author Sam Brannen + * @since 7.1 + * @see MockitoResetTestExecutionListenerWithContextLoadFailureTests + * @see MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests + */ +class MockitoResetTestExecutionListenerTests { + + private final TestContext testContext = mock(); + + + @Test + void beforeTestMethodIsNoOpWhenContextIsNotAvailable() { + when(testContext.hasApplicationContext()).thenReturn(false); + + new MockitoResetTestExecutionListener().beforeTestMethod(testContext); + + verify(testContext).hasApplicationContext(); + verify(testContext, never()).getApplicationContext(); + } + + @Test + void afterTestMethodIsNoOpWhenContextIsNotAvailable() { + when(testContext.hasApplicationContext()).thenReturn(false); + + new MockitoResetTestExecutionListener().afterTestMethod(testContext); + + verify(testContext).hasApplicationContext(); + verify(testContext, never()).getApplicationContext(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests.java new file mode 100644 index 00000000000..d6910930c92 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-present 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 org.junit.jupiter.api.Test; +import org.testng.TestNG; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.testng.TrackingTestNGTestListener; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * JUnit based integration test which verifies that + * {@link MockitoResetTestExecutionListener} — when used in conjunction with + * Spring's TestNG support — does not attempt to load an application context + * to reset mocks if the application context is not currently loaded or previously + * failed to load. + * + * @author Sam Brannen + * @since 7.1 + * @see gh-36782 + * @see MockitoResetTestExecutionListenerWithContextLoadFailureTests + */ +class MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests { + + /** + *

NOTE: The {@code @BeforeMethod(alwaysRun = true)} and {@code @AfterMethod(alwaysRun = true)} + * lifecycle methods in {@link AbstractTestNGSpringContextTests} are always invoked, even if a + * previous lifecycle configuration method failed (for example, due to a context-load failure). + */ + @Test + void contextLoadFailureCausesExpectedTestFailures() { + TrackingTestNGTestListener listener = new TrackingTestNGTestListener(); + TestNG testNG = new TestNG(); + testNG.addListener(listener); + testNG.setTestClasses(new Class[] { ContextLoadFailureTestCase.class }); + testNG.setVerbose(0); + testNG.run(); + + assertThat(listener.testStartCount).as("tests started").hasValue(2); + assertThat(listener.testSuccessCount).as("tests succeeded").hasValue(0); + assertThat(listener.testFailureCount).as("tests failed").hasValue(0); + // Before the introduction of hasApplicationContext() checks in + // MockitoResetTestExecutionListener, the @BeforeMethod and @AfterMethod + // lifecycle methods in AbstractTestNGSpringContextTests also attempted to + // load the faulty ApplicationContext, resulting in 5 configuration failures: + // 1 * @BeforeClass + 2 * @BeforeMethod + 2 * @AfterMethod = 5. + // With the fix, only the @BeforeClass context-load failure is recorded. + assertThat(listener.failedConfigurationsCount).as("failed configurations").hasValue(1); + } + + + /** + *

The {@code @TestExecutionListeners} declaration replaces the default listeners with + * only those needed to exercise {@link MockitoResetTestExecutionListener}, ensuring that + * no additional listeners call {@code testContext.getApplicationContext()} without a + * conditional {@code hasApplicationContext()} check. + */ + @ContextConfiguration + @TestExecutionListeners({ + BeanOverrideTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + MockitoResetTestExecutionListener.class + }) + static class ContextLoadFailureTestCase extends AbstractTestNGSpringContextTests { + + @MockitoBean + ExampleService exampleService; + + @org.testng.annotations.Test + void test1() { + } + + @org.testng.annotations.Test + void test2() { + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String alwaysFails() { + throw new RuntimeException("Simulated context load failure"); + } + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTests.java new file mode 100644 index 00000000000..7f6c0488734 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListenerWithContextLoadFailureTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-present 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 org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +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; + +/** + * Tests which indirectly verify that {@link MockitoResetTestExecutionListener} does not + * attempt to load an application context to reset mocks if the application context + * is not currently loaded or previously failed to load. + * + * @author Sam Brannen + * @since 7.1 + * @see gh-36782 + * @see MockitoResetTestExecutionListenerTests + * @see MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests + */ +class MockitoResetTestExecutionListenerWithContextLoadFailureTests { + + @Test + void contextLoadFailureCausesExpectedTestFailures() { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(ContextLoadFailureTestCase.class)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(2).succeeded(0).failed(2)) + .assertThatEvents() + .haveExactly(1, event(test("test1"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.startsWith("Failed to load ApplicationContext"))))) + .haveExactly(1, event(test("test2"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.contains("failure threshold"))))); + } + + + /** + *

The {@code @TestExecutionListeners} declaration replaces the default listeners with + * only those needed to exercise {@link MockitoResetTestExecutionListener}, ensuring that + * no additional listeners call {@code testContext.getApplicationContext()} without a + * conditional {@code hasApplicationContext()} check. + */ + @SpringJUnitConfig + @TestExecutionListeners({ + BeanOverrideTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + MockitoResetTestExecutionListener.class + }) + @TestMethodOrder(MethodName.class) + static class ContextLoadFailureTestCase { + + @MockitoBean + ExampleService exampleService; + + @Test + void test1() { + } + + @Test + void test2() { + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String alwaysFails() { + throw new RuntimeException("Simulated context load failure"); + } + } + } + +}