Add value attribute alias to @⁠MockitoBean and @⁠MockitoSpyBean

This commit improves the user experience for common use cases by
introducing new `value` attributes in @⁠MockitoBean and
@⁠MockitoSpyBean that are aliases for the existing `name` attributes.

For example, this allows developers to declare
@⁠MockitoBean("userService") instead of @⁠MockitoBean(name =
"userService"), analogous to the existing name/value alias support in
@⁠TestBean.

Closes gh-33680
This commit is contained in:
Sam Brannen
2024-10-10 13:05:33 +02:00
parent 1b8f2c46c1
commit 0e8316e704
12 changed files with 97 additions and 68 deletions

View File

@@ -77,7 +77,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@MockitoBean(name = "service") // <1>
@MockitoBean("service") // <1>
private CustomService customService;
// test case body...
@@ -121,7 +121,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@MockitoSpyBean(name = "service") // <1>
@MockitoSpyBean("service") // <1>
private CustomService customService;
// test case body...

View File

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.mockito.Answers;
import org.mockito.MockSettings;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
@@ -33,12 +34,12 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using a Mockito mock.
*
* <p>By default, the bean to override is inferred from the type of the annotated
* <p>By default, the bean to mock 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 replace by setting the
* {@link #name() name} attribute.
* Alternatively, you can explicitly specify a bean name to mock by setting the
* {@link #value() value} or {@link #name() name} attribute.
*
* <p>A new bean definition will be created if a corresponding bean definition does
* not exist. However, if you would like for the test to fail when a corresponding
@@ -52,7 +53,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* the context alongside the existing dependency.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
* Any attempt to override a non-singleton bean will result in an exception.
* Any attempt to mock a non-singleton bean will result in an exception.
*
* @author Simon Baslé
* @author Sam Brannen
@@ -67,12 +68,22 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface MockitoBean {
/**
* The name of the bean to register or replace.
* <p>If left unspecified, the bean to override is selected according to
* the annotated field's type, taking qualifiers into account if necessary.
* See the {@linkplain MockitoBean class-level documentation} for details.
* @return the name of the mocked bean
* Alias for {@link #name()}.
* <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoBean("customBeanName")}.
* @see #name()
*/
@AliasFor("name")
String value() default "";
/**
* Name of the bean to mock.
* <p>If left unspecified, the bean to mock is selected according to the
* annotated field's type, taking qualifiers into account if necessary. See
* the {@linkplain MockitoBean class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**

View File

@@ -24,28 +24,32 @@ import java.lang.annotation.Target;
import org.mockito.Mockito;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to trigger a bean override using a Mockito spy, which will wrap
* the original instance.
* the original bean instance.
*
* <p>If no explicit {@link #name()} is specified, a target bean is selected
* according to the class of the annotated field, and there must be exactly one
* such candidate bean. A {@code @Qualifier} annotation can be used to help
* disambiguate.
* If a {@link #name()} is specified, it is required that a target bean of that
* name has been previously registered in the application context.
* <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>Dependencies that are known to the application context but are not beans
* (such as those
* <p>A spy cannot be created for components which are known to the application
* context but are not beans &mdash; for example, components
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly}) will not be found.
* registered directly} as resolvable dependencies.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
* Any attempt to override a non-singleton bean will result in an exception.
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied.
* Any attempt to create a spy for a non-singleton bean will result in an exception.
*
* @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
@@ -57,12 +61,22 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface MockitoSpyBean {
/**
* The 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.
* @return the name of the spied bean
* Alias for {@link #name()}.
* <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoSpyBean("customBeanName")}.
* @see #name()
*/
@AliasFor("name")
String value() default "";
/**
* 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.
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**

View File

@@ -17,12 +17,12 @@
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Answers.RETURNS_MOCKS;
/**
* Tests that validate the behavior of {@link MockitoBean} and
@@ -85,7 +85,7 @@ class MockitoBeanContextCustomizerEqualityTests {
static class Case1ByName {
@MockitoBean(name = "serviceBean")
@MockitoBean("serviceBean")
private String exampleService;
}
@@ -99,7 +99,7 @@ class MockitoBeanContextCustomizerEqualityTests {
static class Case2ByName {
@MockitoBean(name = "serviceBean")
@MockitoBean("serviceBean")
private String serviceToMock;
}
@@ -120,14 +120,14 @@ class MockitoBeanContextCustomizerEqualityTests {
static class Case3 {
@MockitoBean(answers = Answers.RETURNS_MOCKS)
@MockitoBean(answers = RETURNS_MOCKS)
private String exampleService;
}
static class Case4ByName {
@MockitoSpyBean(name = "serviceBean")
@MockitoSpyBean("serviceBean")
private String exampleService;
}
@@ -141,7 +141,7 @@ class MockitoBeanContextCustomizerEqualityTests {
static class Case5ByName {
@MockitoSpyBean(name = "serviceBean")
@MockitoSpyBean("serviceBean")
private String serviceToMock;
}

View File

@@ -36,22 +36,22 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig
public class MockitoBeanForByNameLookupIntegrationTests {
@MockitoBean(name = "field")
@MockitoBean("field")
ExampleService field;
@MockitoBean(name = "nestedField")
@MockitoBean("nestedField")
ExampleService nestedField;
@MockitoBean(name = "field")
@MockitoBean("field")
ExampleService renamed1;
@MockitoBean(name = "nestedField")
@MockitoBean("nestedField")
ExampleService renamed2;
@MockitoBean(name = "nonExistingBean")
@MockitoBean("nonExistingBean")
ExampleService nonExisting1;
@MockitoBean(name = "nestedNonExistingBean")
@MockitoBean("nestedNonExistingBean")
ExampleService nonExisting2;

View File

@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.bean.override.OverrideMetadata;
import org.springframework.util.ReflectionUtils;
@@ -114,7 +115,7 @@ class MockitoBeanOverrideMetadataTests {
}
private MockitoBeanOverrideMetadata createMetadata(Field field) {
MockitoBean annotation = field.getAnnotation(MockitoBean.class);
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(field, MockitoBean.class);
return new MockitoBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), annotation);
}
@@ -128,7 +129,7 @@ class MockitoBeanOverrideMetadataTests {
static class SampleOneMockWithName {
@MockitoBean(name = "anotherService")
@MockitoBean("anotherService")
String service;
}
@@ -144,7 +145,7 @@ class MockitoBeanOverrideMetadataTests {
@MockitoBean(name = "beanToMock")
private String service3;
@MockitoBean(name = "beanToMock")
@MockitoBean(value = "beanToMock")
private String service4;
@MockitoBean(extraInterfaces = Externalizable.class)

View File

@@ -26,7 +26,6 @@ import org.junit.jupiter.params.provider.FieldSource;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Events;
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
import org.mockito.quality.Strictness;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
@@ -42,6 +41,7 @@ import static org.junit.platform.testkit.engine.TestExecutionResultConditions.me
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.when;
import static org.mockito.Mockito.mock;
import static org.mockito.quality.Strictness.STRICT_STUBS;
/**
* Integration tests ensuring unnecessary stubbing is reported in various
@@ -85,7 +85,7 @@ class MockitoBeanSettingsStrictIntegrationTests {
@SpringJUnitConfig(Config.class)
@DirtiesContext
@MockitoBeanSettings(Strictness.STRICT_STUBS)
@MockitoBeanSettings(STRICT_STUBS)
static class ExplicitStrictness extends BaseCase {
}

View File

@@ -33,6 +33,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.context.bean.override.mockito.MockReset.BEFORE;
/**
* Integration tests for {@link MockitoBean} that validate automatic reset
@@ -45,10 +46,10 @@ import static org.mockito.Mockito.doReturn;
@TestMethodOrder(OrderAnnotation.class)
public class MockitoBeanWithResetIntegrationTests {
@MockitoBean(reset = MockReset.BEFORE)
@MockitoBean(reset = BEFORE)
ExampleService service;
@MockitoBean(reset = MockReset.BEFORE)
@MockitoBean(reset = BEFORE)
FailingExampleService failingService;
@Order(1)

View File

@@ -40,16 +40,16 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig(Config.class)
public class MockitoSpyBeanForByNameLookupIntegrationTests {
@MockitoSpyBean(name = "field")
@MockitoSpyBean("field")
ExampleService field;
@MockitoSpyBean(name = "nestedField")
@MockitoSpyBean("nestedField")
ExampleService nestedField;
@MockitoSpyBean(name = "field")
@MockitoSpyBean("field")
ExampleService renamed1;
@MockitoSpyBean(name = "nestedField")
@MockitoSpyBean("nestedField")
ExampleService renamed2;

View File

@@ -22,6 +22,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.bean.override.OverrideMetadata;
import org.springframework.util.ReflectionUtils;
@@ -105,7 +106,7 @@ class MockitoSpyBeanOverrideMetadataTests {
}
private MockitoSpyBeanOverrideMetadata createMetadata(Field field) {
MockitoSpyBean annotation = field.getAnnotation(MockitoSpyBean.class);
MockitoSpyBean annotation = AnnotatedElementUtils.getMergedAnnotation(field, MockitoSpyBean.class);
return new MockitoSpyBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), annotation);
}
@@ -119,7 +120,7 @@ class MockitoSpyBeanOverrideMetadataTests {
static class SampleOneSpyWithName {
@MockitoSpyBean(name = "anotherService")
@MockitoSpyBean("anotherService")
String service;
}
@@ -132,10 +133,10 @@ class MockitoSpyBeanOverrideMetadataTests {
@MockitoSpyBean
private String service2;
@MockitoSpyBean(name = "beanToMock")
@MockitoSpyBean(name = "beanToSpy")
private String service3;
@MockitoSpyBean(name = "beanToMock")
@MockitoSpyBean(value = "beanToSpy")
private String service4;
@MockitoSpyBean(reset = MockReset.BEFORE)

View File

@@ -81,7 +81,7 @@ class MockitoSpyBeanTests {
static class ByNameSingleLookup {
@MockitoSpyBean(name = "beanToSpy")
@MockitoSpyBean("beanToSpy")
String example;
}

View File

@@ -22,11 +22,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.FailingExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService;
@@ -35,6 +33,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.context.bean.override.mockito.MockReset.BEFORE;
/**
* Integration tests for {@link MockitoSpyBean} that validate automatic reset
@@ -47,12 +46,13 @@ import static org.mockito.Mockito.doReturn;
@TestMethodOrder(OrderAnnotation.class)
public class MockitoSpyBeanWithResetIntegrationTests {
@MockitoSpyBean(reset = MockReset.BEFORE)
@MockitoSpyBean(reset = BEFORE)
ExampleService service;
@MockitoSpyBean(reset = MockReset.BEFORE)
@MockitoSpyBean(name = "failingExampleServiceFactory", reset = BEFORE)
FailingExampleService failingService;
@Order(1)
@Test
void beanFirstEstablishingStub(ApplicationContext ctx) {
@@ -83,21 +83,23 @@ public class MockitoSpyBeanWithResetIntegrationTests {
@Order(4)
@Test
void factoryBeanSecondEnsuringStubReset(ApplicationContext ctx) {
assertThat(ctx.getBean("factory")).isNotNull().isSameAs(this.failingService);
assertThat(ctx.getBean("failingExampleServiceFactory"))
.isNotNull()
.isSameAs(this.failingService);
assertThatIllegalStateException().isThrownBy(this.failingService::greeting)
.as("not stubbed")
.withMessage("Failed");
}
static class FailingExampleServiceFactory implements FactoryBean<FailingExampleService> {
@Nullable
@Override
public FailingExampleService getObject() {
return new FailingExampleService();
}
@Nullable
@Override
public Class<?> getObjectType() {
return FailingExampleService.class;
@@ -107,14 +109,13 @@ public class MockitoSpyBeanWithResetIntegrationTests {
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean("service")
ExampleService bean1() {
@Bean
ExampleService service() {
return new RealExampleService("Production hello");
}
@Bean("factory")
@Qualifier("factory")
FailingExampleServiceFactory factory() {
@Bean
FailingExampleServiceFactory failingExampleServiceFactory() {
return new FailingExampleServiceFactory();
}
}