Skip to content

Commit 85e1336

Browse files
committed
Support @⁠MockitoSpyBean at the type level on test classes
This commit serves as a Proof of Concept. See spring-projectsgh-34408
1 parent 124b384 commit 85e1336

30 files changed

+548
-44
lines changed

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

+29-15
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnot
4545
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
4646
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
4747
}
48-
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
49-
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
48+
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
49+
Assert.state(mockitoSpyBean.types().length == 0,
50+
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a field");
51+
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean);
5052
}
5153
throw new IllegalStateException("""
5254
Invalid annotation passed to MockitoBeanOverrideProcessor: \
@@ -56,21 +58,33 @@ else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
5658

5759
@Override
5860
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
59-
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
60-
throw new IllegalStateException("""
61-
Invalid annotation passed to MockitoBeanOverrideProcessor: \
62-
expected @MockitoBean on test class """ + testClass.getName());
61+
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
62+
Class<?>[] types = mockitoBean.types();
63+
Assert.state(types.length > 0,
64+
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
65+
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
66+
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
67+
List<BeanOverrideHandler> handlers = new ArrayList<>();
68+
for (Class<?> type : types) {
69+
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
70+
}
71+
return handlers;
6372
}
64-
Class<?>[] types = mockitoBean.types();
65-
Assert.state(types.length > 0,
66-
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
67-
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
68-
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
69-
List<BeanOverrideHandler> handlers = new ArrayList<>();
70-
for (Class<?> type : types) {
71-
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
73+
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
74+
Class<?>[] types = mockitoSpyBean.types();
75+
Assert.state(types.length > 0,
76+
"The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
77+
Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1,
78+
"The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
79+
List<BeanOverrideHandler> handlers = new ArrayList<>();
80+
for (Class<?> type : types) {
81+
handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean));
82+
}
83+
return handlers;
7284
}
73-
return handlers;
85+
throw new IllegalStateException("""
86+
Invalid annotation passed to MockitoBeanOverrideProcessor: \
87+
expected either @MockitoBean or @MockitoSpyBean on test class """ + testClass.getName());
7488
}
7589

7690
}

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Repeatable;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -66,9 +67,10 @@
6667
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
6768
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
6869
*/
69-
@Target(ElementType.FIELD)
70+
@Target({ElementType.FIELD, ElementType.TYPE})
7071
@Retention(RetentionPolicy.RUNTIME)
7172
@Documented
73+
@Repeatable(MockitoSpyBeans.class)
7274
@BeanOverride(MockitoBeanOverrideProcessor.class)
7375
public @interface MockitoSpyBean {
7476

@@ -91,6 +93,19 @@
9193
@AliasFor("value")
9294
String name() default "";
9395

96+
/**
97+
* One or more types to spy.
98+
* <p>Defaults to none.
99+
* <p>Each type specified will result in a spy being created and registered
100+
* with the {@code ApplicationContext}.
101+
* <p>Types must be omitted when the annotation is used on a field.
102+
* <p>When {@code @MockitoSpyBean} also defines a {@link #name}, this attribute
103+
* can only contain a single value.
104+
* @return the types to spy
105+
* @since 6.2.3
106+
*/
107+
Class<?>[] types() default {};
108+
94109
/**
95110
* The reset mode to apply to the spied bean.
96111
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
4848
new SpringAopBypassingVerificationStartedListener();
4949

5050

51-
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
51+
MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) {
52+
this(null, typeToSpy, spyBean);
53+
}
54+
55+
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
5256
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
5357
BeanOverrideStrategy.WRAP, spyBean.reset());
5458
Assert.notNull(typeToSpy, "typeToSpy must not be null");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2002-2025 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.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Container for {@link MockitoSpyBean @MockitoSpyBean} annotations which allows
27+
* {@code @MockitoSpyBean} to be used as a {@linkplain java.lang.annotation.Repeatable
28+
* repeatable annotation} at the type level &mdash; for example, on test classes
29+
* or interfaces implemented by test classes.
30+
*
31+
* @author Sam Brannen
32+
* @since 6.2.3
33+
*/
34+
@Target(ElementType.TYPE)
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Documented
37+
public @interface MockitoSpyBeans {
38+
39+
MockitoSpyBean[] value();
40+
41+
}
+2-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import org.springframework.test.context.bean.override.mockito.MockitoBean;
2020

2121
@MockitoBean(types = Service01.class)
22-
interface TestInterface01 {
22+
interface MockTestInterface01 {
2323
}
+2-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import org.springframework.test.context.bean.override.mockito.MockitoBean;
2020

2121
@MockitoBean(types = Service08.class)
22-
interface TestInterface08 {
22+
interface MockTestInterface08 {
2323
}
+2-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import org.springframework.test.context.bean.override.mockito.MockitoBean;
2020

2121
@MockitoBean(types = Service11.class)
22-
interface TestInterface11 {
22+
interface MockTestInterface11 {
2323
}
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import org.junit.jupiter.api.BeforeEach;
2020
import org.junit.jupiter.api.Test;
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import org.junit.jupiter.api.BeforeEach;
2020
import org.junit.jupiter.api.Nested;
@@ -27,6 +27,7 @@
2727

2828
import static org.assertj.core.api.Assertions.assertThat;
2929
import static org.mockito.BDDMockito.given;
30+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
3031

3132
/**
3233
* Integration tests for {@link MockitoBeans @MockitoBeans} and
@@ -42,7 +43,7 @@
4243
@MockitoBean(types = {Service04.class, Service05.class})
4344
@SharedMocks // Intentionally declared between local @MockitoBean declarations
4445
@MockitoBean(types = Service06.class)
45-
class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
46+
class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
4647

4748
@Autowired
4849
Service01 service01;
@@ -79,6 +80,14 @@ void configureMocks() {
7980

8081
@Test
8182
void checkMocks() {
83+
assertIsMock(service01, "service01");
84+
assertIsMock(service02, "service02");
85+
assertIsMock(service03, "service03");
86+
assertIsMock(service04, "service04");
87+
assertIsMock(service05, "service05");
88+
assertIsMock(service06, "service06");
89+
assertIsMock(service07, "service07");
90+
8291
assertThat(service01.greeting()).isEqualTo("mock 01");
8392
assertThat(service02.greeting()).isEqualTo("mock 02");
8493
assertThat(service03.greeting()).isEqualTo("mock 03");
@@ -90,7 +99,7 @@ void checkMocks() {
9099

91100

92101
@MockitoBean(types = Service09.class)
93-
class BaseTestCase implements TestInterface08 {
102+
class BaseTestCase implements MockTestInterface08 {
94103

95104
@Autowired
96105
Service08 service08;
@@ -104,7 +113,7 @@ class BaseTestCase implements TestInterface08 {
104113

105114
@Nested
106115
@MockitoBean(types = Service12.class)
107-
class NestedTests extends BaseTestCase implements TestInterface11 {
116+
class NestedTests extends BaseTestCase implements MockTestInterface11 {
108117

109118
@Autowired
110119
Service11 service11;
@@ -128,6 +137,20 @@ void configureMocks() {
128137

129138
@Test
130139
void checkMocks() {
140+
assertIsMock(service01, "service01");
141+
assertIsMock(service02, "service02");
142+
assertIsMock(service03, "service03");
143+
assertIsMock(service04, "service04");
144+
assertIsMock(service05, "service05");
145+
assertIsMock(service06, "service06");
146+
assertIsMock(service07, "service07");
147+
assertIsMock(service08, "service08");
148+
assertIsMock(service09, "service09");
149+
assertIsMock(service10, "service10");
150+
assertIsMock(service11, "service11");
151+
assertIsMock(service12, "service12");
152+
assertIsMock(service13, "service13");
153+
131154
assertThat(service01.greeting()).isEqualTo("mock 01");
132155
assertThat(service02.greeting()).isEqualTo("mock 02");
133156
assertThat(service03.greeting()).isEqualTo("mock 03");
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.test.context.bean.override.mockito.mockbeans;
17+
package org.springframework.test.context.bean.override.mockito.typelevel;
1818

1919
import java.util.stream.Stream;
2020

0 commit comments

Comments
 (0)