Support @MockitoSpyBean at the type level on test classes
Prior to this commit, @MockitoSpyBean could only be declared on fields within test classes, which prevented developers from being able to easily reuse spy configuration across a test suite. With this commit, @MockitoSpyBean is now supported at the type level on test classes, their superclasses, and interfaces implemented by those classes. @MockitoSpyBean is also supported on enclosing classes for @Nested test classes, their superclasses, and interfaces implemented by those classes, while honoring @NestedTestConfiguration semantics. In addition, @MockitoSpyBean: - has a new `types` attribute that can be used to declare the type or types to spy when @MockitoSpyBean is declared at the type level - can be declared as a repeatable annotation at the type level - can be declared as a meta-annotation on a custom composed annotation which can be reused across a test suite (see the @SharedSpies example in the reference manual) To support these new features, this commit also includes the following changes. - MockitoSpyBeanOverrideProcessor has been revised to support @MockitoSpyBean at the type level. - The "Bean Overriding in Tests" and "@MockitoBean and @MockitoSpyBean" sections of the reference manual have been fully revised. See gh-34408 Closes gh-33925
This commit is contained in:
@@ -31,9 +31,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @MockitoBean} is an annotation that can be used in test classes to
|
||||
* override beans in a test's
|
||||
* override a bean in the test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* using Mockito mocks.
|
||||
* with a Mockito mock.
|
||||
*
|
||||
* <p>{@code @MockitoBean} can be applied in the following ways.
|
||||
* <ul>
|
||||
@@ -49,18 +49,19 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* </ul>
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
|
||||
* from the type of the annotated field. If multiple candidates exist, a
|
||||
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
|
||||
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
|
||||
* field will be used as a fallback qualifier. Alternatively, you can explicitly
|
||||
* specify a bean name to mock by setting the {@link #value() value} or
|
||||
* {@link #name() name} attribute.
|
||||
* from the type of the annotated field. If multiple candidates exist in the
|
||||
* {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
|
||||
* on the field to help disambiguate. In the absence of a {@code @Qualifier}
|
||||
* annotation, the name of the annotated field will be used as a <em>fallback
|
||||
* qualifier</em>. Alternatively, you can explicitly specify a bean name to mock
|
||||
* by setting the {@link #value() value} or {@link #name() name} attribute.
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
|
||||
* to mock must be supplied via the {@link #types() types} attribute. If multiple
|
||||
* candidates exist, you can explicitly specify a bean name to mock by setting the
|
||||
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
|
||||
* must contain a single type if an explicit bean {@code name} is configured.
|
||||
* (or beans) to mock must be supplied via the {@link #types() types} attribute.
|
||||
* If multiple candidates exist in the {@code ApplicationContext}, you can
|
||||
* explicitly specify a bean name to mock by setting the {@link #name() name}
|
||||
* attribute. Note, however, that the {@code types} attribute must contain a
|
||||
* single type if an explicit bean {@code name} is configured.
|
||||
*
|
||||
* <p>A bean will be created if a corresponding bean does not exist. However, if
|
||||
* you would like for the test to fail when a corresponding bean does not exist,
|
||||
@@ -111,7 +112,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
public @interface MockitoBean {
|
||||
|
||||
/**
|
||||
* Alias for {@link #name()}.
|
||||
* Alias for {@link #name() name}.
|
||||
* <p>Intended to be used when no other attributes are needed — for
|
||||
* example, {@code @MockitoBean("customBeanName")}.
|
||||
* @see #name()
|
||||
@@ -136,7 +137,7 @@ public @interface MockitoBean {
|
||||
* <p>Each type specified will result in a mock being created and registered
|
||||
* with the {@code ApplicationContext}.
|
||||
* <p>Types must be omitted when the annotation is used on a field.
|
||||
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
|
||||
* <p>When {@code @MockitoBean} also defines a {@link #name name}, this attribute
|
||||
* can only contain a single value.
|
||||
* @return the types to mock
|
||||
* @since 6.2.2
|
||||
|
||||
@@ -45,8 +45,10 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
|
||||
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
|
||||
}
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
|
||||
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
|
||||
Assert.state(mockitoSpyBean.types().length == 0,
|
||||
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a field");
|
||||
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean);
|
||||
}
|
||||
throw new IllegalStateException("""
|
||||
Invalid annotation passed to MockitoBeanOverrideProcessor: \
|
||||
@@ -56,21 +58,34 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
@Override
|
||||
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
|
||||
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
|
||||
throw new IllegalStateException("""
|
||||
Invalid annotation passed to MockitoBeanOverrideProcessor: \
|
||||
expected @MockitoBean on test class """ + testClass.getName());
|
||||
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
|
||||
Class<?>[] types = mockitoBean.types();
|
||||
Assert.state(types.length > 0,
|
||||
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
|
||||
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
|
||||
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
for (Class<?> type : types) {
|
||||
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
Class<?>[] types = mockitoBean.types();
|
||||
Assert.state(types.length > 0,
|
||||
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
|
||||
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
|
||||
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
for (Class<?> type : types) {
|
||||
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
|
||||
Class<?>[] types = mockitoSpyBean.types();
|
||||
Assert.state(types.length > 0,
|
||||
"The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
|
||||
Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1,
|
||||
"The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
for (Class<?> type : types) {
|
||||
handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean));
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
return handlers;
|
||||
throw new IllegalStateException("""
|
||||
Invalid annotation passed to MockitoBeanOverrideProcessor: \
|
||||
expected either @MockitoBean or @MockitoSpyBean on test class %s"""
|
||||
.formatted(testClass.getName()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
@@ -26,19 +27,40 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @MockitoSpyBean} is an annotation that can be applied to a non-static
|
||||
* field in a test class to override a bean in the test's
|
||||
* {@code @MockitoSpyBean} is an annotation that can be used in test classes to
|
||||
* override a bean in the test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* with a Mockito spy that wraps the original bean instance.
|
||||
*
|
||||
* <p>By default, the bean to spy is inferred from the type of the annotated
|
||||
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
|
||||
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
|
||||
* the name of the annotated field will be used as a fallback qualifier.
|
||||
* Alternatively, you can explicitly specify a bean name to spy by setting the
|
||||
* {@link #value() value} or {@link #name() name} attribute. If a bean name is
|
||||
* specified, it is required that a target bean with that name has been previously
|
||||
* registered in the application context.
|
||||
* <p>{@code @MockitoSpyBean} can be applied in the following ways.
|
||||
* <ul>
|
||||
* <li>On a non-static field in a test class or any of its superclasses.</li>
|
||||
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
|
||||
* or in any class in the type hierarchy or enclosing class hierarchy above the
|
||||
* {@code @Nested} test class.</li>
|
||||
* <li>At the type level on a test class or any superclass or implemented interface
|
||||
* in the type hierarchy above the test class.</li>
|
||||
* <li>At the type level on an enclosing class for a {@code @Nested} test class
|
||||
* or on any class or interface in the type hierarchy or enclosing class hierarchy
|
||||
* above the {@code @Nested} test class.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When {@code @MockitoSpyBean} is declared on a field, the bean to spy is
|
||||
* inferred from the type of the annotated field. If multiple candidates exist in
|
||||
* the {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
|
||||
* on the field to help disambiguate. In the absence of a {@code @Qualifier}
|
||||
* annotation, the name of the annotated field will be used as a <em>fallback
|
||||
* qualifier</em>. Alternatively, you can explicitly specify a bean name to spy
|
||||
* by setting the {@link #value() value} or {@link #name() name} attribute. If a
|
||||
* bean name is specified, it is required that a target bean with that name has
|
||||
* been previously registered in the application context.
|
||||
*
|
||||
* <p>When {@code @MockitoSpyBean} is declared at the type level, the type of bean
|
||||
* (or beans) to spy must be supplied via the {@link #types() types} attribute.
|
||||
* If multiple candidates exist in the {@code ApplicationContext}, you can
|
||||
* explicitly specify a bean name to spy by setting the {@link #name() name}
|
||||
* attribute. Note, however, that the {@code types} attribute must contain a
|
||||
* single type if an explicit bean {@code name} is configured.
|
||||
*
|
||||
* <p>A spy cannot be created for components which are known to the application
|
||||
* context but are not beans — for example, components
|
||||
@@ -56,24 +78,33 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* (default visibility), or {@code private} depending on the needs or coding
|
||||
* practices of the project.
|
||||
*
|
||||
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
|
||||
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* <p>{@code @MockitoSpyBean} fields and type-level {@code @MockitoSpyBean} declarations
|
||||
* will be inherited from an enclosing test class by default. See
|
||||
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* for details.
|
||||
*
|
||||
* <p>{@code @MockitoSpyBean} may be used as a <em>meta-annotation</em> to create
|
||||
* custom <em>composed annotations</em> — for example, to define common spy
|
||||
* configuration in a single annotation that can be reused across a test suite.
|
||||
* {@code @MockitoSpyBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
|
||||
* annotation at the type level — for example, to spy on several beans by
|
||||
* {@link #name() name}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
|
||||
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Target({ElementType.FIELD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Repeatable(MockitoSpyBeans.class)
|
||||
@BeanOverride(MockitoBeanOverrideProcessor.class)
|
||||
public @interface MockitoSpyBean {
|
||||
|
||||
/**
|
||||
* Alias for {@link #name()}.
|
||||
* Alias for {@link #name() name}.
|
||||
* <p>Intended to be used when no other attributes are needed — for
|
||||
* example, {@code @MockitoSpyBean("customBeanName")}.
|
||||
* @see #name()
|
||||
@@ -84,13 +115,27 @@ public @interface MockitoSpyBean {
|
||||
/**
|
||||
* Name of the bean to spy.
|
||||
* <p>If left unspecified, the bean to spy is selected according to the
|
||||
* annotated field's type, taking qualifiers into account if necessary. See
|
||||
* the {@linkplain MockitoSpyBean class-level documentation} for details.
|
||||
* configured {@link #types() types} or the annotated field's type, taking
|
||||
* qualifiers into account if necessary. See the {@linkplain MockitoSpyBean
|
||||
* class-level documentation} for details.
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* One or more types to spy.
|
||||
* <p>Defaults to none.
|
||||
* <p>Each type specified will result in a spy being created and registered
|
||||
* with the {@code ApplicationContext}.
|
||||
* <p>Types must be omitted when the annotation is used on a field.
|
||||
* <p>When {@code @MockitoSpyBean} also defines a {@link #name name}, this
|
||||
* attribute can only contain a single value.
|
||||
* @return the types to spy
|
||||
* @since 6.2.3
|
||||
*/
|
||||
Class<?>[] types() default {};
|
||||
|
||||
/**
|
||||
* The reset mode to apply to the spied bean.
|
||||
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically
|
||||
|
||||
@@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
|
||||
new SpringAopBypassingVerificationStartedListener();
|
||||
|
||||
|
||||
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
|
||||
MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) {
|
||||
this(null, typeToSpy, spyBean);
|
||||
}
|
||||
|
||||
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
|
||||
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
|
||||
BeanOverrideStrategy.WRAP, spyBean.reset());
|
||||
Assert.notNull(typeToSpy, "typeToSpy must not be null");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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 java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Container for {@link MockitoSpyBean @MockitoSpyBean} annotations which allows
|
||||
* {@code @MockitoSpyBean} to be used as a {@linkplain java.lang.annotation.Repeatable
|
||||
* repeatable annotation} at the type level — for example, on test classes
|
||||
* or interfaces implemented by test classes.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.3
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface MockitoSpyBeans {
|
||||
|
||||
MockitoSpyBean[] value();
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user