Reset mocks only when a test has an ApplicationContext

Prior to this commit and the previous commit,
MockitoResetTestExecutionListener always attempted to load the
ApplicationContext to reset mocks in its beforeTestMethod() and
afterTestMethod() callbacks, even if there was no active
ApplicationContext.

The reason this was noticed is that the @BeforeMethod(alwaysRun = true)
and @AfterMethod(alwaysRun = true) lifecycle methods in
AbstractTestNGSpringContextTests are always invoked, even if a previous
lifecycle configuration method failed (for example, due to a
context-load failure).

However, with JUnit Jupiter and the SpringExtension the
beforeTestMethod() and afterTestMethod() callbacks in the
TestExecutionListener API are not invoked if there was a previous
lifecycle failure.

Consequently, the reported drawbacks only exist when using Spring's
TestNG base support classes

This commit picks up where the previous commit left off by applying the
same hasApplicationContext() check in beforeTestMethod().

This commit also introduces unit and integration tests for both Jupiter
and TestNG support.

Closes gh-36782
This commit is contained in:
Sam Brannen
2026-05-20 14:04:09 +02:00
parent 1d91982f83
commit d87c03a6be
4 changed files with 279 additions and 1 deletions
@@ -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);
}
}
@@ -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();
}
}
@@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36782">gh-36782</a>
* @see MockitoResetTestExecutionListenerWithContextLoadFailureTests
*/
class MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests {
/**
* <p>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);
}
/**
* <p>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");
}
}
}
}
@@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36782">gh-36782</a>
* @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")))));
}
/**
* <p>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");
}
}
}
}