Skip to content

Commit d87c03a

Browse files
committed
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
1 parent 1d91982 commit d87c03a

4 files changed

Lines changed: 279 additions & 1 deletion

File tree

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public int getOrder() {
9292

9393
@Override
9494
public void beforeTestMethod(TestContext testContext) {
95-
if (isEnabled()) {
95+
if (isEnabled() && testContext.hasApplicationContext()) {
9696
resetMocks(testContext.getApplicationContext(), MockReset.BEFORE);
9797
}
9898
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.test.context.TestContext;
22+
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.never;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
/**
29+
* Unit tests for {@link MockitoResetTestExecutionListener}.
30+
*
31+
* @author Sam Brannen
32+
* @since 7.1
33+
* @see MockitoResetTestExecutionListenerWithContextLoadFailureTests
34+
* @see MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests
35+
*/
36+
class MockitoResetTestExecutionListenerTests {
37+
38+
private final TestContext testContext = mock();
39+
40+
41+
@Test
42+
void beforeTestMethodIsNoOpWhenContextIsNotAvailable() {
43+
when(testContext.hasApplicationContext()).thenReturn(false);
44+
45+
new MockitoResetTestExecutionListener().beforeTestMethod(testContext);
46+
47+
verify(testContext).hasApplicationContext();
48+
verify(testContext, never()).getApplicationContext();
49+
}
50+
51+
@Test
52+
void afterTestMethodIsNoOpWhenContextIsNotAvailable() {
53+
when(testContext.hasApplicationContext()).thenReturn(false);
54+
55+
new MockitoResetTestExecutionListener().afterTestMethod(testContext);
56+
57+
verify(testContext).hasApplicationContext();
58+
verify(testContext, never()).getApplicationContext();
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.testng.TestNG;
21+
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.test.context.ContextConfiguration;
25+
import org.springframework.test.context.TestExecutionListeners;
26+
import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener;
27+
import org.springframework.test.context.bean.override.example.ExampleService;
28+
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
29+
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
30+
import org.springframework.test.context.testng.TrackingTestNGTestListener;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* JUnit based integration test which verifies that
36+
* {@link MockitoResetTestExecutionListener} — when used in conjunction with
37+
* Spring's TestNG support — does not attempt to load an application context
38+
* to reset mocks if the application context is not currently loaded or previously
39+
* failed to load.
40+
*
41+
* @author Sam Brannen
42+
* @since 7.1
43+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36782">gh-36782</a>
44+
* @see MockitoResetTestExecutionListenerWithContextLoadFailureTests
45+
*/
46+
class MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests {
47+
48+
/**
49+
* <p>NOTE: The {@code @BeforeMethod(alwaysRun = true)} and {@code @AfterMethod(alwaysRun = true)}
50+
* lifecycle methods in {@link AbstractTestNGSpringContextTests} are always invoked, even if a
51+
* previous lifecycle configuration method failed (for example, due to a context-load failure).
52+
*/
53+
@Test
54+
void contextLoadFailureCausesExpectedTestFailures() {
55+
TrackingTestNGTestListener listener = new TrackingTestNGTestListener();
56+
TestNG testNG = new TestNG();
57+
testNG.addListener(listener);
58+
testNG.setTestClasses(new Class<?>[] { ContextLoadFailureTestCase.class });
59+
testNG.setVerbose(0);
60+
testNG.run();
61+
62+
assertThat(listener.testStartCount).as("tests started").hasValue(2);
63+
assertThat(listener.testSuccessCount).as("tests succeeded").hasValue(0);
64+
assertThat(listener.testFailureCount).as("tests failed").hasValue(0);
65+
// Before the introduction of hasApplicationContext() checks in
66+
// MockitoResetTestExecutionListener, the @BeforeMethod and @AfterMethod
67+
// lifecycle methods in AbstractTestNGSpringContextTests also attempted to
68+
// load the faulty ApplicationContext, resulting in 5 configuration failures:
69+
// 1 * @BeforeClass + 2 * @BeforeMethod + 2 * @AfterMethod = 5.
70+
// With the fix, only the @BeforeClass context-load failure is recorded.
71+
assertThat(listener.failedConfigurationsCount).as("failed configurations").hasValue(1);
72+
}
73+
74+
75+
/**
76+
* <p>The {@code @TestExecutionListeners} declaration replaces the default listeners with
77+
* only those needed to exercise {@link MockitoResetTestExecutionListener}, ensuring that
78+
* no additional listeners call {@code testContext.getApplicationContext()} without a
79+
* conditional {@code hasApplicationContext()} check.
80+
*/
81+
@ContextConfiguration
82+
@TestExecutionListeners({
83+
BeanOverrideTestExecutionListener.class,
84+
DependencyInjectionTestExecutionListener.class,
85+
MockitoResetTestExecutionListener.class
86+
})
87+
static class ContextLoadFailureTestCase extends AbstractTestNGSpringContextTests {
88+
89+
@MockitoBean
90+
ExampleService exampleService;
91+
92+
@org.testng.annotations.Test
93+
void test1() {
94+
}
95+
96+
@org.testng.annotations.Test
97+
void test2() {
98+
}
99+
100+
@Configuration(proxyBeanMethods = false)
101+
static class Config {
102+
103+
@Bean
104+
String alwaysFails() {
105+
throw new RuntimeException("Simulated context load failure");
106+
}
107+
}
108+
}
109+
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.MethodOrderer.MethodName;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.TestMethodOrder;
22+
import org.junit.platform.testkit.engine.EngineTestKit;
23+
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.test.context.TestExecutionListeners;
27+
import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener;
28+
import org.springframework.test.context.bean.override.example.ExampleService;
29+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
30+
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
31+
32+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
33+
import static org.junit.platform.testkit.engine.EventConditions.event;
34+
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
35+
import static org.junit.platform.testkit.engine.EventConditions.test;
36+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
37+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
38+
39+
/**
40+
* Tests which indirectly verify that {@link MockitoResetTestExecutionListener} does not
41+
* attempt to load an application context to reset mocks if the application context
42+
* is not currently loaded or previously failed to load.
43+
*
44+
* @author Sam Brannen
45+
* @since 7.1
46+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36782">gh-36782</a>
47+
* @see MockitoResetTestExecutionListenerTests
48+
* @see MockitoResetTestExecutionListenerWithContextLoadFailureTestNGTests
49+
*/
50+
class MockitoResetTestExecutionListenerWithContextLoadFailureTests {
51+
52+
@Test
53+
void contextLoadFailureCausesExpectedTestFailures() {
54+
EngineTestKit.engine("junit-jupiter")
55+
.selectors(selectClass(ContextLoadFailureTestCase.class))
56+
.execute()
57+
.testEvents()
58+
.assertStatistics(stats -> stats.started(2).succeeded(0).failed(2))
59+
.assertThatEvents()
60+
.haveExactly(1, event(test("test1"),
61+
finishedWithFailure(
62+
instanceOf(IllegalStateException.class),
63+
message(msg -> msg.startsWith("Failed to load ApplicationContext")))))
64+
.haveExactly(1, event(test("test2"),
65+
finishedWithFailure(
66+
instanceOf(IllegalStateException.class),
67+
message(msg -> msg.contains("failure threshold")))));
68+
}
69+
70+
71+
/**
72+
* <p>The {@code @TestExecutionListeners} declaration replaces the default listeners with
73+
* only those needed to exercise {@link MockitoResetTestExecutionListener}, ensuring that
74+
* no additional listeners call {@code testContext.getApplicationContext()} without a
75+
* conditional {@code hasApplicationContext()} check.
76+
*/
77+
@SpringJUnitConfig
78+
@TestExecutionListeners({
79+
BeanOverrideTestExecutionListener.class,
80+
DependencyInjectionTestExecutionListener.class,
81+
MockitoResetTestExecutionListener.class
82+
})
83+
@TestMethodOrder(MethodName.class)
84+
static class ContextLoadFailureTestCase {
85+
86+
@MockitoBean
87+
ExampleService exampleService;
88+
89+
@Test
90+
void test1() {
91+
}
92+
93+
@Test
94+
void test2() {
95+
}
96+
97+
@Configuration(proxyBeanMethods = false)
98+
static class Config {
99+
100+
@Bean
101+
String alwaysFails() {
102+
throw new RuntimeException("Simulated context load failure");
103+
}
104+
}
105+
}
106+
107+
}

0 commit comments

Comments
 (0)