Skip to content

Commit 54948a4

Browse files
committed
Log warning when one Bean Override overrides another Bean Override
It is currently possible for one Bean Override to override another logically equivalent Bean Override. For example, a @⁠TestBean can override a @⁠MockitoBean, and vice versa. In fact, it's also possible for a @⁠MockitoBean to override another @⁠MockitoBean, for a @⁠TestBean to override a @⁠TestBean, etc. However, there may be viable use cases for one override overriding another override. For example, one may have a need to spy on a bean created by a @⁠TestBean factory method. In light of that, we do not prohibit one Bean Override from overriding another Bean Override; however, with this commit we now log a warning to help developers diagnose issues in case such an override is unintentional. For example, given the following test class, where TestConfig registers a single bean of type MyService named "myService"... @⁠SpringJUnitConfig(TestConfig.class) class MyTests { @⁠TestBean(methodName = "example.TestUtils#createMyService") MyService testService; @⁠MockitoBean MyService mockService; @⁠Test void test() { // ... } } ... running that test class results in a log message similar to the following, which has been formatted for readability. WARN - Bean with name 'myService' was overridden by multiple handlers: [ [TestBeanOverrideHandler@44b21f9f field = example.MyService example.MyTests.testService, beanType = example.MyService, beanName = [null], strategy = REPLACE_OR_CREATE ], [MockitoBeanOverrideHandler@7ee8130e field = example.MyService example.MyTests.mockService, beanType = example.MyService, beanName = [null], strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers = RETURNS_DEFAULTS, serializable = false ] ] NOTE: The last registered BeanOverrideHandler wins. In the above example, that means that @⁠MockitoBean overrides @⁠TestBean, resulting in a Mockito mock for the MyService bean in the test's ApplicationContext. Closes gh-34056
1 parent a00ba8d commit 54948a4

File tree

5 files changed

+252
-17
lines changed

5 files changed

+252
-17
lines changed

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

+29-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
package org.springframework.test.context.bean.override;
1818

1919
import java.lang.reflect.Field;
20-
import java.util.HashMap;
20+
import java.util.LinkedHashMap;
21+
import java.util.List;
2122
import java.util.Map;
23+
import java.util.Map.Entry;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
2227

2328
import org.springframework.beans.factory.BeanCreationException;
2429
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@@ -37,9 +42,12 @@
3742
*/
3843
class BeanOverrideRegistry {
3944

40-
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new HashMap<>();
45+
private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class);
46+
4147

42-
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new HashMap<>();
48+
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new LinkedHashMap<>();
49+
50+
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new LinkedHashMap<>();
4351

4452
private final ConfigurableBeanFactory beanFactory;
4553

@@ -57,7 +65,25 @@ class BeanOverrideRegistry {
5765
* bean via {@link #wrapBeanIfNecessary(Object, String)}.
5866
*/
5967
void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) {
68+
Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () ->
69+
"Cannot register BeanOverrideHandler for bean with name '%s'; detected multiple registrations for %s"
70+
.formatted(beanName, handler));
71+
72+
// Check if beanName was already registered, before adding the new mapping.
73+
boolean beanNameAlreadyRegistered = this.handlerToBeanNameMap.containsValue(beanName);
74+
// Add new mapping before potentially logging a warning, to ensure that
75+
// the current handler is logged as well.
6076
this.handlerToBeanNameMap.put(handler, beanName);
77+
78+
if (beanNameAlreadyRegistered && logger.isWarnEnabled()) {
79+
List<BeanOverrideHandler> competingHandlers = this.handlerToBeanNameMap.entrySet().stream()
80+
.filter(entry -> entry.getValue().equals(beanName))
81+
.map(Entry::getKey)
82+
.toList();
83+
logger.warn("Bean with name '%s' was overridden by multiple handlers: %s"
84+
.formatted(beanName, competingHandlers));
85+
}
86+
6187
if (handler.getStrategy() == BeanOverrideStrategy.WRAP) {
6288
this.wrappingBeanOverrideHandlers.put(beanName, handler);
6389
}

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeCreationIntegrationTests.java

+10-7
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,37 @@
2525
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
2626

2727
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
2829

2930
/**
3031
* Integration tests for {@link MockitoBean @MockitoBean} where duplicate mocks
31-
* are created for the same nonexistent type.
32+
* are created for the same nonexistent type, selected by-type.
3233
*
3334
* @author Sam Brannen
3435
* @since 6.2.1
3536
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34025">gh-34025</a>
37+
* @see MockitoBeanDuplicateTypeReplacementIntegrationTests
3638
* @see MockitoSpyBeanDuplicateTypeIntegrationTests
37-
* @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests
3839
*/
3940
@SpringJUnitConfig
40-
public class MockitoBeanDuplicateTypeIntegrationTests {
41+
public class MockitoBeanDuplicateTypeCreationIntegrationTests {
4142

4243
@MockitoBean
43-
ExampleService service1;
44+
ExampleService mock1;
4445

4546
@MockitoBean
46-
ExampleService service2;
47+
ExampleService mock2;
4748

4849
@Autowired
4950
List<ExampleService> services;
5051

5152

5253
@Test
5354
void duplicateMocksShouldHaveBeenCreated() {
54-
assertThat(service1).isNotSameAs(service2);
55-
assertThat(services).hasSize(2);
55+
assertThat(services).containsExactly(mock1, mock2);
56+
assertThat(mock1).isNotSameAs(mock2);
57+
assertIsMock(mock1);
58+
assertIsMock(mock2);
5659
}
5760

5861
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2002-2024 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 java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.test.context.bean.override.example.ExampleService;
27+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.mockito.BDDMockito.given;
31+
import static org.springframework.test.context.bean.override.mockito.MockReset.AFTER;
32+
import static org.springframework.test.context.bean.override.mockito.MockReset.BEFORE;
33+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
34+
35+
/**
36+
* Integration tests for {@link MockitoBean @MockitoBean} where duplicate mocks
37+
* are created to replace the same existing bean, selected by-type.
38+
*
39+
* <p>In other words, this test class demonstrates how one {@code @MockitoBean}
40+
* can silently override another {@code @MockitoBean}.
41+
*
42+
* @author Sam Brannen
43+
* @since 6.2.1
44+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34056">gh-34056</a>
45+
* @see MockitoBeanDuplicateTypeCreationIntegrationTests
46+
* @see MockitoSpyBeanDuplicateTypeIntegrationTests
47+
*/
48+
@SpringJUnitConfig
49+
public class MockitoBeanDuplicateTypeReplacementIntegrationTests {
50+
51+
@MockitoBean(reset = AFTER)
52+
ExampleService mock1;
53+
54+
@MockitoBean(reset = BEFORE)
55+
ExampleService mock2;
56+
57+
@Autowired
58+
List<ExampleService> services;
59+
60+
/**
61+
* One could argue that we would ideally expect an exception to be thrown when
62+
* two competing mocks are created to replace the same existing bean; however,
63+
* we currently only log a warning in such cases.
64+
* <p>This method therefore asserts the status quo in terms of behavior.
65+
* <p>And the log can be manually checked to verify that an appropriate
66+
* warning was logged.
67+
*/
68+
@Test
69+
void onlyOneMockShouldHaveBeenCreated() {
70+
// Currently logs something similar to the following.
71+
//
72+
// WARN - Bean with name 'exampleService' was overridden by multiple handlers:
73+
// [MockitoBeanOverrideHandler@5478ce1e ..., MockitoBeanOverrideHandler@5edc70ed ...]
74+
75+
// Last one wins: there's only one physical mock
76+
assertThat(services).containsExactly(mock2);
77+
assertThat(mock1).isSameAs(mock2);
78+
79+
assertIsMock(mock2);
80+
assertThat(MockReset.get(mock2)).as("MockReset").isEqualTo(BEFORE);
81+
82+
assertThat(mock2.greeting()).isNull();
83+
given(mock2.greeting()).willReturn("mocked");
84+
assertThat(mock2.greeting()).isEqualTo("mocked");
85+
}
86+
87+
88+
@Configuration
89+
static class Config {
90+
91+
@Bean
92+
ExampleService exampleService() {
93+
return () -> "@Bean";
94+
}
95+
}
96+
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2002-2024 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 java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.test.context.bean.override.convention.TestBean;
27+
import org.springframework.test.context.bean.override.example.ExampleService;
28+
import org.springframework.test.context.bean.override.example.RealExampleService;
29+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.mockito.BDDMockito.given;
33+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
34+
35+
/**
36+
* Integration tests for Bean Overrides where a {@link MockitoBean @MockitoBean}
37+
* overrides a {@link TestBean @TestBean} when trying to replace the same existing
38+
* bean, selected by-type.
39+
*
40+
* <p>In other words, this test class demonstrates how one Bean Override can
41+
* silently override another Bean Override.
42+
*
43+
* @author Sam Brannen
44+
* @since 6.2.1
45+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34056">gh-34056</a>
46+
* @see MockitoBeanDuplicateTypeCreationIntegrationTests
47+
* @see MockitoBeanDuplicateTypeReplacementIntegrationTests
48+
*/
49+
@SpringJUnitConfig
50+
public class MockitoBeanOverridesTestBeanIntegrationTests {
51+
52+
@TestBean
53+
ExampleService testService;
54+
55+
@MockitoBean
56+
ExampleService mockService;
57+
58+
@Autowired
59+
List<ExampleService> services;
60+
61+
62+
static ExampleService testService() {
63+
return new RealExampleService("@TestBean");
64+
}
65+
66+
67+
/**
68+
* One could argue that we would ideally expect an exception to be thrown when
69+
* two competing overrides are created to replace the same existing bean; however,
70+
* we currently only log a warning in such cases.
71+
* <p>This method therefore asserts the status quo in terms of behavior.
72+
* <p>And the log can be manually checked to verify that an appropriate
73+
* warning was logged.
74+
*/
75+
@Test
76+
void mockitoBeanShouldOverrideTestBean() {
77+
// Currently logs something similar to the following.
78+
//
79+
// WARN - Bean with name 'exampleService' was overridden by multiple handlers:
80+
// [TestBeanOverrideHandler@770beef5 ..., MockitoBeanOverrideHandler@6dd1f638 ...]
81+
82+
// Last override wins...
83+
assertThat(services).containsExactly(mockService);
84+
assertThat(testService).isSameAs(mockService);
85+
86+
assertIsMock(mockService);
87+
88+
assertThat(mockService.greeting()).isNull();
89+
given(mockService.greeting()).willReturn("mocked");
90+
assertThat(mockService.greeting()).isEqualTo("mocked");
91+
}
92+
93+
94+
@Configuration
95+
static class Config {
96+
97+
@Bean
98+
ExampleService exampleService() {
99+
return () -> "@Bean";
100+
}
101+
}
102+
103+
}

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

+13-7
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,34 @@
3636
*
3737
* @author Sam Brannen
3838
* @since 6.2.1
39-
* @see MockitoBeanDuplicateTypeIntegrationTests
39+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34056">gh-34056</a>
40+
* @see MockitoBeanDuplicateTypeCreationIntegrationTests
4041
* @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests
4142
*/
4243
@SpringJUnitConfig
4344
public class MockitoSpyBeanDuplicateTypeIntegrationTests {
4445

4546
@MockitoSpyBean
46-
ExampleService service1;
47+
ExampleService spy1;
4748

4849
@MockitoSpyBean
49-
ExampleService service2;
50+
ExampleService spy2;
5051

5152
@Autowired
5253
List<ExampleService> services;
5354

5455

5556
@Test
56-
void test() {
57-
assertThat(service1).isSameAs(service2);
58-
assertThat(services).containsExactly(service1);
57+
void onlyOneSpyShouldHaveBeenCreated() {
58+
// Currently logs something similar to the following.
59+
//
60+
// WARN - Bean with name 'exampleService' was overridden by multiple handlers:
61+
// [MockitoSpyBeanOverrideHandler@1d269ed7 ..., MockitoSpyBeanOverrideHandler@437ebf59 ...]
5962

60-
assertIsSpy(service1, "service1");
63+
assertThat(services).containsExactly(spy2);
64+
assertThat(spy1).isSameAs(spy2);
65+
66+
assertIsSpy(spy2);
6167
}
6268

6369

0 commit comments

Comments
 (0)