Improve Bean Overriding support, testing and documentation
This commit improves on the bean overriding feature in several ways: the API is simplified and polished (metadata and processor contracts, etc...). The commit also reworks infrastructure classes (context customizer, test execution listener, BeanOverrideBeanFactoryPostProcessor, etc...). Parsing of annotations is now fully stateless. In order to avoid OverrideMetadata in bean definition and to make a first step towards AOT support, the BeanOverrideBeanFactoryPostProcessor now delegates to a BeanOverrideRegistrar to track classes to parse, the metadata-related state as well as for the field injection methods for tests. Lastly, this commit increases the test coverage for the provided annotations and adds integration tests and fixes a few `@TestBean` issues.
This commit is contained in:
parent
711ddd1ac6
commit
2d33aac350
|
@ -47,6 +47,9 @@ Java::
|
|||
<2> The result of this static method will be used as the instance and injected into the field
|
||||
======
|
||||
|
||||
NOTE: The method to invoke is searched in the test class and any enclosing class it might
|
||||
have, as well as its hierarchy. This typically allows nested test class to provide the
|
||||
method to use in the root test class.
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean]]
|
||||
== `@MockitoBean` and `@MockitoSpyBean`
|
||||
|
@ -100,32 +103,31 @@ and associated infrastructure, which allows to define custom bean overriding var
|
|||
|
||||
In order to provide an extension, three classes are needed:
|
||||
|
||||
- A concrete `BeanOverrideProcessor<P>`.
|
||||
- A concrete `OverrideMetadata` created by said processor.
|
||||
- A concrete `BeanOverrideProcessor` implementation, `P`.
|
||||
- One or more concrete `OverrideMetadata` implementations created by said processor.
|
||||
- An annotation meta-annotated with `@BeanOverride(P.class)`.
|
||||
|
||||
The Spring TestContext Framework includes infrastructure classes that support bean
|
||||
overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
|
||||
These are automatically registered via the Spring TestContext Framework `spring.factories`
|
||||
file.
|
||||
overriding: a `BeanFactoryPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
|
||||
The later two are automatically registered via the Spring TestContext Framework
|
||||
`spring.factories` file, and are responsible for setting up the rest of the infrastructure.
|
||||
|
||||
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
|
||||
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
|
||||
|
||||
Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context,
|
||||
registering and replacing bean definitions as influenced by each metadata
|
||||
Then the `BeanOverrideBeanFactoryPostProcessor` will use that information to alter the
|
||||
Context, registering and replacing bean definitions as influenced by each metadata
|
||||
`BeanOverrideStrategy`:
|
||||
|
||||
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
|
||||
If it is not present in the context, an exception is thrown.
|
||||
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
|
||||
in the context, one is created
|
||||
- `WRAP_EARLY_BEAN`: an original instance is obtained via
|
||||
`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and
|
||||
provided to the processor during `OverrideMetadata` creation.
|
||||
- `WRAP_EARLY_BEAN`: an original instance is obtained and passed to the `OverrideMetadata`
|
||||
when the override instance is created.
|
||||
|
||||
NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't
|
||||
include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the
|
||||
name of the bean to override MUST be somehow provided to or computed by the
|
||||
`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom
|
||||
annotation's attributes, or the annotated field's name.
|
||||
NOTE: The Bean Overriding infrastructure doesn't include any bean resolution step
|
||||
(unlike e.g. an `@Autowired`-annotated field). As such, the name of the bean to override
|
||||
MUST be somehow provided to or computed by the `BeanOverrideProcessor`. Typically, the end
|
||||
user provides the name as part of the custom annotation's attributes, or the annotated
|
||||
field's name.
|
|
@ -34,7 +34,7 @@ import java.lang.annotation.Target;
|
|||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
* @see BeanOverrideBeanPostProcessor
|
||||
* @see BeanOverrideBeanFactoryPostProcessor
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.PriorityOrdered;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link BeanFactoryPostProcessor} implementation that processes test classes
|
||||
* and adapt the {@link BeanDefinitionRegistry} for any {@link BeanOverride} it
|
||||
* may define.
|
||||
*
|
||||
* <p>A set of classes from which to parse {@link OverrideMetadata} must be
|
||||
* provided to this processor. Each test class is expected to use any
|
||||
* annotation meta-annotated with {@link BeanOverride @BeanOverride} to mark
|
||||
* beans to override. The {@link BeanOverrideParsingUtils#hasBeanOverride(Class)}
|
||||
* method can be used to check if a class matches the above criteria.
|
||||
*
|
||||
* <p>The provided classes are fully parsed at creation to build a metadata set.
|
||||
* This processor implements several {@link BeanOverrideStrategy overriding
|
||||
* strategy} and chooses the correct one according to each override metadata
|
||||
* {@link OverrideMetadata#getStrategy()} method. Additionally, it provides
|
||||
* support for injecting the overridden bean instances into their corresponding
|
||||
* annotated {@link Field fields}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
*/
|
||||
class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
|
||||
|
||||
private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanFactoryPostProcessor.class.getName();
|
||||
|
||||
private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
|
||||
BeanOverrideBeanFactoryPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
|
||||
|
||||
private final BeanOverrideRegistrar overrideRegistrar;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with
|
||||
* the given {@link BeanOverrideRegistrar}, which contains a set of parsed
|
||||
* {@link OverrideMetadata}.
|
||||
* @param overrideRegistrar the {@link BeanOverrideRegistrar} used to track
|
||||
* metadata
|
||||
*/
|
||||
public BeanOverrideBeanFactoryPostProcessor(BeanOverrideRegistrar overrideRegistrar) {
|
||||
this.overrideRegistrar = overrideRegistrar;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE - 10;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
Assert.isInstanceOf(DefaultListableBeanFactory.class, beanFactory,
|
||||
"Bean overriding annotations can only be used on a DefaultListableBeanFactory");
|
||||
postProcessWithRegistry((DefaultListableBeanFactory) beanFactory);
|
||||
}
|
||||
|
||||
private void postProcessWithRegistry(DefaultListableBeanFactory registry) {
|
||||
for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) {
|
||||
registerBeanOverride(registry, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy certain details of a {@link BeanDefinition} to the definition created by
|
||||
* this processor for a given {@link OverrideMetadata}.
|
||||
* <p>The default implementation copies the {@linkplain BeanDefinition#isPrimary()
|
||||
* primary flag} and the {@linkplain BeanDefinition#getScope() scope}.
|
||||
*/
|
||||
protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) {
|
||||
to.setPrimary(from.isPrimary());
|
||||
to.setScope(from.getScope());
|
||||
}
|
||||
|
||||
private void registerBeanOverride(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
|
||||
switch (overrideMetadata.getStrategy()) {
|
||||
case REPLACE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, true);
|
||||
case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, false);
|
||||
case WRAP_BEAN -> registerWrapBean(beanFactory, overrideMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReplaceDefinition(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
|
||||
|
||||
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
|
||||
String beanName = overrideMetadata.getBeanName();
|
||||
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (beanFactory.containsBeanDefinition(beanName)) {
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
|
||||
beanFactory.removeBeanDefinition(beanName);
|
||||
}
|
||||
else if (enforceExistingDefinition) {
|
||||
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
|
||||
" bean definition to replace with that name");
|
||||
}
|
||||
beanFactory.registerBeanDefinition(beanName, beanDefinition);
|
||||
|
||||
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
|
||||
if (beanFactory.isSingleton(beanName)) {
|
||||
// Now we have an instance (the override) that we can register.
|
||||
// At this stage we don't expect a singleton instance to be present,
|
||||
// and this call will throw if there is such an instance already.
|
||||
beanFactory.registerSingleton(beanName, override);
|
||||
}
|
||||
|
||||
overrideMetadata.track(override, beanFactory);
|
||||
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the expected bean name is registered and matches the type to override.
|
||||
* <p>If so, put the override metadata in the early tracking map.
|
||||
* <p>The map will later be checked to see if a given bean should be wrapped
|
||||
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
|
||||
* phase.
|
||||
*/
|
||||
private void registerWrapBean(DefaultListableBeanFactory beanFactory, OverrideMetadata metadata) {
|
||||
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
|
||||
String beanName = metadata.getBeanName();
|
||||
if (!existingBeanNames.contains(beanName)) {
|
||||
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," +
|
||||
" no existing bean instance by this name of type " + metadata.getBeanType());
|
||||
}
|
||||
this.overrideRegistrar.markWrapEarly(metadata, beanName);
|
||||
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
|
||||
}
|
||||
|
||||
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition();
|
||||
definition.setTargetType(metadata.getBeanType());
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Set<String> getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) {
|
||||
Set<String> beans = new LinkedHashSet<>(
|
||||
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
|
||||
Class<?> type = resolvableType.resolve(Object.class);
|
||||
for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) {
|
||||
beanName = BeanFactoryUtils.transformedBeanName(beanName);
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
|
||||
if (resolvableType.equals(attribute) || type.equals(attribute)) {
|
||||
beans.add(beanName);
|
||||
}
|
||||
}
|
||||
beans.removeIf(ScopedProxyUtils::isScopedTarget);
|
||||
return beans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link BeanOverrideBeanFactoryPostProcessor} with a {@link BeanDefinitionRegistry}.
|
||||
* <p>Not required when using the Spring TestContext Framework, as registration
|
||||
* is automatic via the
|
||||
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
|
||||
* mechanism.
|
||||
* @param registry the bean definition registry
|
||||
*/
|
||||
public static void register(BeanDefinitionRegistry registry) {
|
||||
RuntimeBeanReference registrarReference = new RuntimeBeanReference(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
|
||||
// Early processor
|
||||
addInfrastructureBeanDefinition(
|
||||
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
|
||||
constructorArgs.addIndexedArgumentValue(0, registrarReference));
|
||||
|
||||
// Main processor
|
||||
addInfrastructureBeanDefinition(
|
||||
registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
|
||||
constructorArgs.addIndexedArgumentValue(0, registrarReference));
|
||||
}
|
||||
|
||||
private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
|
||||
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
|
||||
|
||||
if (!registry.containsBeanDefinition(beanName)) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(clazz);
|
||||
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
|
||||
constructorArgumentsConsumer.accept(constructorArguments);
|
||||
registry.registerBeanDefinition(beanName, definition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
|
||||
PriorityOrdered {
|
||||
|
||||
private final BeanOverrideRegistrar overrideRegistrar;
|
||||
|
||||
private final Map<String, Object> earlyReferences;
|
||||
|
||||
|
||||
private WrapEarlyBeanPostProcessor(BeanOverrideRegistrar registrar) {
|
||||
this.overrideRegistrar = registrar;
|
||||
this.earlyReferences = new ConcurrentHashMap<>(16);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof FactoryBean) {
|
||||
return bean;
|
||||
}
|
||||
this.earlyReferences.put(getCacheKey(bean, beanName), bean);
|
||||
return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof FactoryBean) {
|
||||
return bean;
|
||||
}
|
||||
if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) {
|
||||
return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private String getCacheKey(Object bean, String beanName) {
|
||||
return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,382 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.PriorityOrdered;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link BeanFactoryPostProcessor} used to register and inject overriding
|
||||
* bean metadata with the {@link ApplicationContext}.
|
||||
*
|
||||
* <p>A set of {@link OverrideMetadata} must be provided to this processor. A
|
||||
* {@link BeanOverrideParser} can typically be used to parse this metadata from
|
||||
* test classes that use any annotation meta-annotated with
|
||||
* {@link BeanOverride @BeanOverride} to mark override sites.
|
||||
*
|
||||
* <p>This processor supports two types of {@link BeanOverrideStrategy}:
|
||||
* <ul>
|
||||
* <li>Replacing a given bean's definition, immediately preparing a singleton
|
||||
* instance</li>
|
||||
* <li>Intercepting the actual bean instance upon creation and wrapping it,
|
||||
* using the early bean definition mechanism of
|
||||
* {@link SmartInstantiationAwareBeanPostProcessor}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>This processor also provides support for injecting the overridden bean
|
||||
* instances into their corresponding annotated {@link Field fields}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
*/
|
||||
public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPostProcessor,
|
||||
BeanFactoryAware, BeanFactoryPostProcessor, Ordered {
|
||||
|
||||
private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName();
|
||||
|
||||
private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
|
||||
BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
|
||||
|
||||
|
||||
private final Map<String, OverrideMetadata> earlyOverrideMetadata = new HashMap<>();
|
||||
|
||||
private final Map<OverrideMetadata, String> beanNameRegistry = new HashMap<>();
|
||||
|
||||
private final Map<Field, String> fieldRegistry = new HashMap<>();
|
||||
|
||||
private final Set<OverrideMetadata> overrideMetadata;
|
||||
|
||||
@Nullable
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code BeanOverrideBeanPostProcessor} instance with the
|
||||
* given {@link OverrideMetadata} set.
|
||||
* @param overrideMetadata the initial override metadata
|
||||
*/
|
||||
public BeanOverrideBeanPostProcessor(Set<OverrideMetadata> overrideMetadata) {
|
||||
this.overrideMetadata = overrideMetadata;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE - 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory,
|
||||
"Bean overriding can only be used with a ConfigurableListableBeanFactory");
|
||||
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this processor's {@link OverrideMetadata} set.
|
||||
*/
|
||||
protected Set<OverrideMetadata> getOverrideMetadata() {
|
||||
return this.overrideMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
Assert.state(this.beanFactory == beanFactory, "Unexpected BeanFactory to post-process");
|
||||
Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,
|
||||
"Bean overriding annotations can only be used on bean factories that implement "
|
||||
+ "BeanDefinitionRegistry");
|
||||
postProcessWithRegistry((BeanDefinitionRegistry) beanFactory);
|
||||
}
|
||||
|
||||
private void postProcessWithRegistry(BeanDefinitionRegistry registry) {
|
||||
// Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed.
|
||||
for (OverrideMetadata metadata : getOverrideMetadata()) {
|
||||
registerBeanOverride(registry, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy certain details of a {@link BeanDefinition} to the definition created by
|
||||
* this processor for a given {@link OverrideMetadata}.
|
||||
* <p>The default implementation copies the {@linkplain BeanDefinition#isPrimary()
|
||||
* primary flag} and the {@linkplain BeanDefinition#getScope() scope}.
|
||||
*/
|
||||
protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) {
|
||||
to.setPrimary(from.isPrimary());
|
||||
to.setScope(from.getScope());
|
||||
}
|
||||
|
||||
private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata) {
|
||||
switch (overrideMetadata.getBeanOverrideStrategy()) {
|
||||
case REPLACE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, true);
|
||||
case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, false);
|
||||
case WRAP_EARLY_BEAN -> registerWrapEarly(overrideMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata,
|
||||
boolean enforceExistingDefinition) {
|
||||
|
||||
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
|
||||
String beanName = overrideMetadata.getExpectedBeanName();
|
||||
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (registry.containsBeanDefinition(beanName)) {
|
||||
existingBeanDefinition = registry.getBeanDefinition(beanName);
|
||||
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
|
||||
registry.removeBeanDefinition(beanName);
|
||||
}
|
||||
else if (enforceExistingDefinition) {
|
||||
throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() +
|
||||
" bean; expected a bean definition to replace with name '" + beanName + "'");
|
||||
}
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
|
||||
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
|
||||
Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
|
||||
if (this.beanFactory.isSingleton(beanName)) {
|
||||
// Now we have an instance (the override) that we can register.
|
||||
// At this stage we don't expect a singleton instance to be present,
|
||||
// and this call will throw if there is such an instance already.
|
||||
this.beanFactory.registerSingleton(beanName, override);
|
||||
}
|
||||
|
||||
overrideMetadata.track(override, this.beanFactory);
|
||||
this.beanNameRegistry.put(overrideMetadata, beanName);
|
||||
this.fieldRegistry.put(overrideMetadata.field(), beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the expected bean name is registered and matches the type to override.
|
||||
* <p>If so, put the override metadata in the early tracking map.
|
||||
* <p>The map will later be checked to see if a given bean should be wrapped
|
||||
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
|
||||
* phase.
|
||||
*/
|
||||
private void registerWrapEarly(OverrideMetadata metadata) {
|
||||
Set<String> existingBeanNames = getExistingBeanNames(metadata.typeToOverride());
|
||||
String beanName = metadata.getExpectedBeanName();
|
||||
if (!existingBeanNames.contains(beanName)) {
|
||||
throw new IllegalStateException("Unable to override wrap-early bean named '" + beanName + "', not found among " +
|
||||
existingBeanNames);
|
||||
}
|
||||
this.earlyOverrideMetadata.put(beanName, metadata);
|
||||
this.beanNameRegistry.put(metadata, beanName);
|
||||
this.fieldRegistry.put(metadata.field(), beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check early override records and use the {@link OverrideMetadata} to
|
||||
* create an override instance from the provided bean, if relevant.
|
||||
* <p>Called during the {@link SmartInstantiationAwareBeanPostProcessor}
|
||||
* phases.
|
||||
* @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)
|
||||
* @see WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String)
|
||||
*/
|
||||
protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
|
||||
final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
|
||||
if (metadata != null && metadata.getBeanOverrideStrategy() == BeanOverrideStrategy.WRAP_EARLY_BEAN) {
|
||||
bean = metadata.createOverride(beanName, null, bean);
|
||||
Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
|
||||
metadata.track(bean, this.beanFactory);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(metadata.typeToOverride().resolve());
|
||||
definition.setTargetType(metadata.typeToOverride());
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Set<String> getExistingBeanNames(ResolvableType resolvableType) {
|
||||
Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
|
||||
Set<String> beans = new LinkedHashSet<>(
|
||||
Arrays.asList(this.beanFactory.getBeanNamesForType(resolvableType, true, false)));
|
||||
Class<?> type = resolvableType.resolve(Object.class);
|
||||
for (String beanName : this.beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) {
|
||||
beanName = BeanFactoryUtils.transformedBeanName(beanName);
|
||||
BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName);
|
||||
Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
|
||||
if (resolvableType.equals(attribute) || type.equals(attribute)) {
|
||||
beans.add(beanName);
|
||||
}
|
||||
}
|
||||
beans.removeIf(ScopedProxyUtils::isScopedTarget);
|
||||
return beans;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
|
||||
throws BeansException {
|
||||
ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field));
|
||||
return pvs;
|
||||
}
|
||||
|
||||
private void postProcessField(Object bean, Field field) {
|
||||
String beanName = this.fieldRegistry.get(field);
|
||||
if (StringUtils.hasText(beanName)) {
|
||||
inject(field, bean, beanName);
|
||||
}
|
||||
}
|
||||
|
||||
void inject(Field field, Object target, OverrideMetadata overrideMetadata) {
|
||||
String beanName = this.beanNameRegistry.get(overrideMetadata);
|
||||
Assert.state(StringUtils.hasLength(beanName),
|
||||
() -> "No bean found for OverrideMetadata: " + overrideMetadata);
|
||||
inject(field, target, beanName);
|
||||
}
|
||||
|
||||
private void inject(Field field, Object target, String beanName) {
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
Object existingValue = ReflectionUtils.getField(field, target);
|
||||
Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
|
||||
Object bean = this.beanFactory.getBean(beanName, field.getType());
|
||||
if (existingValue == bean) {
|
||||
return;
|
||||
}
|
||||
Assert.state(existingValue == null, () -> "The existing value '" + existingValue +
|
||||
"' of field '" + field + "' is not the same as the new value '" + bean + "'");
|
||||
ReflectionUtils.setField(field, target, bean);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException("Could not inject field '" + field + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link BeanOverrideBeanPostProcessor} with a {@link BeanDefinitionRegistry}.
|
||||
* <p>Not required when using the Spring TestContext Framework, as registration
|
||||
* is automatic via the
|
||||
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
|
||||
* mechanism.
|
||||
* @param registry the bean definition registry
|
||||
* @param overrideMetadata the initial override metadata set
|
||||
*/
|
||||
public static void register(BeanDefinitionRegistry registry, @Nullable Set<OverrideMetadata> overrideMetadata) {
|
||||
// Early processor
|
||||
getOrAddInfrastructureBeanDefinition(
|
||||
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
|
||||
constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME)));
|
||||
|
||||
// Main processor
|
||||
BeanDefinition definition = getOrAddInfrastructureBeanDefinition(
|
||||
registry, BeanOverrideBeanPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
|
||||
constructorArgs.addIndexedArgumentValue(0, new LinkedHashSet<OverrideMetadata>()));
|
||||
ConstructorArgumentValues.ValueHolder constructorArg =
|
||||
definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
|
||||
@SuppressWarnings({"unchecked", "NullAway"})
|
||||
Set<OverrideMetadata> existing = (Set<OverrideMetadata>) constructorArg.getValue();
|
||||
if (overrideMetadata != null && existing != null) {
|
||||
existing.addAll(overrideMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
|
||||
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
|
||||
|
||||
if (!registry.containsBeanDefinition(beanName)) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(clazz);
|
||||
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
|
||||
constructorArgumentsConsumer.accept(constructorArguments);
|
||||
registry.registerBeanDefinition(beanName, definition);
|
||||
return definition;
|
||||
}
|
||||
return registry.getBeanDefinition(beanName);
|
||||
}
|
||||
|
||||
|
||||
private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
|
||||
PriorityOrdered {
|
||||
|
||||
private final BeanOverrideBeanPostProcessor mainProcessor;
|
||||
|
||||
private final Map<String, Object> earlyReferences;
|
||||
|
||||
|
||||
private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) {
|
||||
this.mainProcessor = mainProcessor;
|
||||
this.earlyReferences = new ConcurrentHashMap<>(16);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof FactoryBean) {
|
||||
return bean;
|
||||
}
|
||||
this.earlyReferences.put(getCacheKey(bean, beanName), bean);
|
||||
return this.mainProcessor.wrapIfNecessary(bean, beanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof FactoryBean) {
|
||||
return bean;
|
||||
}
|
||||
if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) {
|
||||
return this.mainProcessor.wrapIfNecessary(bean, beanName);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private String getCacheKey(Object bean, String beanName) {
|
||||
return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -29,56 +30,51 @@ import org.springframework.test.context.MergedContextConfiguration;
|
|||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizerFactory} which provides support for Bean Overriding
|
||||
* in tests.
|
||||
* {@link ContextCustomizerFactory} implementation that provides support for
|
||||
* Bean Overriding.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
* @see BeanOverride
|
||||
*/
|
||||
public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ContextCustomizer createContextCustomizer(Class<?> testClass,
|
||||
List<ContextConfigurationAttributes> configAttributes) {
|
||||
|
||||
BeanOverrideParser parser = new BeanOverrideParser();
|
||||
parseMetadata(testClass, parser);
|
||||
if (parser.getOverrideMetadata().isEmpty()) {
|
||||
Set<Class<?>> detectedClasses = new LinkedHashSet<>();
|
||||
findClassesWithBeanOverride(testClass, detectedClasses);
|
||||
if (detectedClasses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BeanOverrideContextCustomizer(parser.getOverrideMetadata());
|
||||
return new BeanOverrideContextCustomizer(detectedClasses);
|
||||
}
|
||||
|
||||
private void parseMetadata(Class<?> testClass, BeanOverrideParser parser) {
|
||||
parser.parse(testClass);
|
||||
private void findClassesWithBeanOverride(Class<?> testClass, Set<Class<?>> detectedClasses) {
|
||||
if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) {
|
||||
detectedClasses.add(testClass);
|
||||
}
|
||||
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
|
||||
parseMetadata(testClass.getEnclosingClass(), parser);
|
||||
findClassesWithBeanOverride(testClass.getEnclosingClass(), detectedClasses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizer} for Bean Overriding in tests.
|
||||
*/
|
||||
private static final class BeanOverrideContextCustomizer implements ContextCustomizer {
|
||||
|
||||
private final Set<OverrideMetadata> metadata;
|
||||
private final Set<Class<?>> detectedClasses;
|
||||
|
||||
/**
|
||||
* Construct a context customizer given some pre-existing override
|
||||
* metadata.
|
||||
* @param metadata a set of concrete {@link OverrideMetadata} provided
|
||||
* by the underlying {@link BeanOverrideParser}
|
||||
*/
|
||||
BeanOverrideContextCustomizer(Set<OverrideMetadata> metadata) {
|
||||
this.metadata = metadata;
|
||||
BeanOverrideContextCustomizer(Set<Class<?>> detectedClasses) {
|
||||
this.detectedClasses = detectedClasses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
|
||||
if (context instanceof BeanDefinitionRegistry registry) {
|
||||
BeanOverrideBeanPostProcessor.register(registry, this.metadata);
|
||||
BeanOverrideRegistrar.register(registry, this.detectedClasses);
|
||||
BeanOverrideBeanFactoryPostProcessor.register(registry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,12 +87,12 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFa
|
|||
return false;
|
||||
}
|
||||
BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj;
|
||||
return this.metadata.equals(other.metadata);
|
||||
return this.detectedClasses.equals(other.detectedClasses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.metadata.hashCode();
|
||||
return this.detectedClasses.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,12 @@ package org.springframework.test.context.bean.override;
|
|||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -33,63 +32,58 @@ import org.springframework.util.ReflectionUtils;
|
|||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT;
|
||||
|
||||
/**
|
||||
* A parser that discovers annotations meta-annotated with {@link BeanOverride @BeanOverride}
|
||||
* on fields of a given class and creates {@link OverrideMetadata} accordingly.
|
||||
* Internal parsing utilities to discover the presence of
|
||||
* {@link BeanOverride @BeanOverride} on fields, and create the relevant
|
||||
* {@link OverrideMetadata} accordingly.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
class BeanOverrideParser {
|
||||
|
||||
private final Set<OverrideMetadata> parsedMetadata = new LinkedHashSet<>();
|
||||
|
||||
abstract class BeanOverrideParsingUtils {
|
||||
|
||||
/**
|
||||
* Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called.
|
||||
*/
|
||||
Set<OverrideMetadata> getOverrideMetadata() {
|
||||
return Collections.unmodifiableSet(this.parsedMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover fields of the provided class that are meta-annotated with
|
||||
* {@link BeanOverride @BeanOverride}, then instantiate the corresponding
|
||||
* {@link BeanOverrideProcessor} and use it to create {@link OverrideMetadata}
|
||||
* for each field.
|
||||
* <p>Each call to {@code parse} adds the parsed metadata to the parser's
|
||||
* override metadata {@link #getOverrideMetadata() set}.
|
||||
* @param testClass the test class in which to inspect fields
|
||||
*/
|
||||
void parse(Class<?> testClass) {
|
||||
ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any field of the provided {@code testClass} is meta-annotated
|
||||
* Check if at least one field of the given {@code clazz} is meta-annotated
|
||||
* with {@link BeanOverride @BeanOverride}.
|
||||
* <p>This is similar to the initial discovery of fields in {@link #parse(Class)}
|
||||
* without the heavier steps of instantiating processors and creating
|
||||
* {@link OverrideMetadata}. Consequently, this method leaves the current
|
||||
* state of the parser's override metadata {@link #getOverrideMetadata() set}
|
||||
* unchanged.
|
||||
* @param testClass the class which fields to inspect
|
||||
* @return true if there is a bean override annotation present, false otherwise
|
||||
* @see #parse(Class)
|
||||
* @param clazz the class which fields to inspect
|
||||
* @return {@code true} if there is a bean override annotation present,
|
||||
* {@code false} otherwise
|
||||
*/
|
||||
boolean hasBeanOverride(Class<?> testClass) {
|
||||
static boolean hasBeanOverride(Class<?> clazz) {
|
||||
AtomicBoolean hasBeanOverride = new AtomicBoolean();
|
||||
ReflectionUtils.doWithFields(testClass, field -> {
|
||||
ReflectionUtils.doWithFields(clazz, field -> {
|
||||
if (hasBeanOverride.get()) {
|
||||
return;
|
||||
}
|
||||
boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class);
|
||||
hasBeanOverride.compareAndSet(false, present);
|
||||
});
|
||||
return hasBeanOverride.get();
|
||||
if (hasBeanOverride.get()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseField(Field field, Class<?> source) {
|
||||
/**
|
||||
* Parse the specified classes for the presence of fields annotated with
|
||||
* {@link BeanOverride @BeanOverride}, and create an {@link OverrideMetadata}
|
||||
* for each.
|
||||
* @param classes the classes to parse
|
||||
*/
|
||||
static Set<OverrideMetadata> parse(Iterable<Class<?>> classes) {
|
||||
Set<OverrideMetadata> result = new LinkedHashSet<>();
|
||||
classes.forEach(c -> ReflectionUtils.doWithFields(c, field -> parseField(field, c, result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to {@link #parse(Iterable) parse} a single test class.
|
||||
*/
|
||||
static Set<OverrideMetadata> parse(Class<?> clazz) {
|
||||
return parse(List.of(clazz));
|
||||
}
|
||||
|
||||
private static void parseField(Field field, Class<?> testClass, Set<OverrideMetadata> metadataSet) {
|
||||
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
|
||||
|
||||
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
|
||||
|
@ -100,14 +94,11 @@ class BeanOverrideParser {
|
|||
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
|
||||
Assert.state(metaSource != null, "Meta-annotation source must not be null");
|
||||
Annotation composedAnnotation = metaSource.synthesize();
|
||||
ResolvableType typeToOverride = processor.getOrDeduceType(field, composedAnnotation, source);
|
||||
|
||||
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
|
||||
() -> "Multiple @BeanOverride annotations found on field: " + field);
|
||||
OverrideMetadata metadata = processor.createMetadata(field, composedAnnotation, typeToOverride);
|
||||
boolean isNewDefinition = this.parsedMetadata.add(metadata);
|
||||
Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() +
|
||||
" OverrideMetadata: " + metadata);
|
||||
OverrideMetadata metadata = processor.createMetadata(composedAnnotation, testClass, field);
|
||||
metadataSet.add(metadata);
|
||||
});
|
||||
}
|
||||
|
|
@ -18,18 +18,14 @@ package org.springframework.test.context.bean.override;
|
|||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Strategy interface for Bean Override processing.
|
||||
* Strategy interface for Bean Override processing and creation of
|
||||
* {@link OverrideMetadata}.
|
||||
*
|
||||
* <p>Processors are generally linked to one or more specific concrete annotations
|
||||
* (meta-annotated with {@link BeanOverride @BeanOverride}) and specify different
|
||||
* steps in the process of parsing these annotations, ultimately creating
|
||||
* {@link OverrideMetadata} which will be used to instantiate the overrides.
|
||||
* <p>Processors are generally linked to one or more specific concrete
|
||||
* annotations (meta-annotated with {@link BeanOverride @BeanOverride}) and
|
||||
* concrete {@link OverrideMetadata} implementations.
|
||||
*
|
||||
* <p>Implementations are required to have a no-argument constructor and be
|
||||
* stateless.
|
||||
|
@ -41,31 +37,13 @@ import org.springframework.core.ResolvableType;
|
|||
public interface BeanOverrideProcessor {
|
||||
|
||||
/**
|
||||
* Determine the {@link ResolvableType} for which an {@link OverrideMetadata}
|
||||
* instance will be created — for example, by using the supplied annotation
|
||||
* to determine the type.
|
||||
* <p>The default implementation deduces the field's corresponding
|
||||
* {@link ResolvableType}, additionally tracking the source class if the
|
||||
* field's type is a {@link TypeVariable}.
|
||||
*/
|
||||
default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class<?> source) {
|
||||
return (field.getGenericType() instanceof TypeVariable ?
|
||||
ResolvableType.forField(field, source) : ResolvableType.forField(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link OverrideMetadata} instance for the given annotated field
|
||||
* and target {@link #getOrDeduceType(Field, Annotation, Class) type}.
|
||||
* <p>Specific implementations of metadata can have state to be used during
|
||||
* override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition,
|
||||
* Object) instance creation} — for example, from further parsing of the
|
||||
* annotation or the annotated field.
|
||||
* @param field the annotated field
|
||||
* Create an {@link OverrideMetadata} instance for the given annotated field.
|
||||
* @param overrideAnnotation the field annotation
|
||||
* @param typeToOverride the target type
|
||||
* @param testClass the test class being processed, which can be different
|
||||
* from the {@code field.getDeclaringClass()} in case the field is inherited
|
||||
* from a superclass
|
||||
* @param field the annotated field
|
||||
* @return a new {@link OverrideMetadata} instance
|
||||
* @see #getOrDeduceType(Field, Annotation, Class)
|
||||
*/
|
||||
OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride);
|
||||
|
||||
OverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An internal class used to track {@link OverrideMetadata}-related state after
|
||||
* the bean factory has been processed and to provide field injection utilities
|
||||
* for test execution listeners.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
*/
|
||||
class BeanOverrideRegistrar implements BeanFactoryAware {
|
||||
|
||||
static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideRegistrar.class.getName();
|
||||
|
||||
private final Map<OverrideMetadata, String> beanNameRegistry;
|
||||
private final Map<String, OverrideMetadata> earlyOverrideMetadata;
|
||||
private final Set<OverrideMetadata> overrideMetadata;
|
||||
|
||||
@Nullable
|
||||
private ConfigurableBeanFactory beanFactory;
|
||||
|
||||
/**
|
||||
* Construct a new registrar and immediately parse the provided classes.
|
||||
* @param classesToParse the initial set of classes that have been
|
||||
* detected to contain bean overriding annotations, to be parsed immediately.
|
||||
*/
|
||||
BeanOverrideRegistrar(Set<Class<?>> classesToParse) {
|
||||
Set<OverrideMetadata> metadata = BeanOverrideParsingUtils.parse(classesToParse);
|
||||
Assert.state(!metadata.isEmpty(), "Expected metadata to be produced by parser");
|
||||
this.overrideMetadata = metadata;
|
||||
this.beanNameRegistry = new HashMap<>();
|
||||
this.earlyOverrideMetadata = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this processor's {@link OverrideMetadata} set.
|
||||
*/
|
||||
Set<OverrideMetadata> getOverrideMetadata() {
|
||||
return this.overrideMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
Assert.isInstanceOf(ConfigurableBeanFactory.class, beanFactory,
|
||||
"Bean OverrideRegistrar can only be used with a ConfigurableBeanFactory");
|
||||
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check {@link #markWrapEarly(OverrideMetadata, String) early override}
|
||||
* records and use the {@link OverrideMetadata} to create an override
|
||||
* instance from the provided bean, if relevant.
|
||||
*/
|
||||
final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
|
||||
final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
|
||||
if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) {
|
||||
bean = metadata.createOverride(beanName, null, bean);
|
||||
Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null");
|
||||
metadata.track(bean, this.beanFactory);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the provided {@link OverrideMetadata} and associated it with a
|
||||
* {@code beanName}.
|
||||
*/
|
||||
void registerNameForMetadata(OverrideMetadata metadata, String beanName) {
|
||||
this.beanNameRegistry.put(metadata, beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the provided {@link OverrideMetadata} and {@code beanName} as "wrap
|
||||
* early", allowing for later bean override using {@link #wrapIfNecessary(Object, String)}.
|
||||
*/
|
||||
public void markWrapEarly(OverrideMetadata metadata, String beanName) {
|
||||
this.earlyOverrideMetadata.put(beanName, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a bean definition for a {@link BeanOverrideRegistrar} if it does
|
||||
* not yet exist. Additionally, each call adds the provided
|
||||
* {@code detectedTestClasses} to the set that will be used as constructor
|
||||
* argument.
|
||||
* <p>The resulting complete set of test classes will be parsed as soon as
|
||||
* the {@link BeanOverrideRegistrar} is constructed.
|
||||
* @param registry the bean definition registry
|
||||
* @param detectedTestClasses a partial {@link Set} of {@link Class classes}
|
||||
* that are expected to contain bean overriding annotations
|
||||
*/
|
||||
public static void register(BeanDefinitionRegistry registry, @Nullable Set<Class<?>> detectedTestClasses) {
|
||||
BeanDefinition definition;
|
||||
if (!registry.containsBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME)) {
|
||||
definition = new RootBeanDefinition(BeanOverrideRegistrar.class);
|
||||
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
|
||||
constructorArguments.addIndexedArgumentValue(0, new LinkedHashSet<Class<?>>());
|
||||
registry.registerBeanDefinition(INFRASTRUCTURE_BEAN_NAME, definition);
|
||||
}
|
||||
else {
|
||||
definition = registry.getBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
|
||||
}
|
||||
|
||||
ConstructorArgumentValues.ValueHolder constructorArg =
|
||||
definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
|
||||
@SuppressWarnings({"unchecked", "NullAway"})
|
||||
Set<Class<?>> existing = (Set<Class<?>>) constructorArg.getValue();
|
||||
if (detectedTestClasses != null && existing != null) {
|
||||
existing.addAll(detectedTestClasses);
|
||||
}
|
||||
}
|
||||
|
||||
void inject(Object target, OverrideMetadata overrideMetadata) {
|
||||
String beanName = this.beanNameRegistry.get(overrideMetadata);
|
||||
Assert.state(StringUtils.hasLength(beanName),
|
||||
() -> "No bean found for OverrideMetadata: " + overrideMetadata);
|
||||
inject(overrideMetadata.getField(), target, beanName);
|
||||
}
|
||||
|
||||
private void inject(Field field, Object target, String beanName) {
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
Object existingValue = ReflectionUtils.getField(field, target);
|
||||
Assert.state(this.beanFactory != null, "beanFactory must not be null");
|
||||
Object bean = this.beanFactory.getBean(beanName, field.getType());
|
||||
if (existingValue == bean) {
|
||||
return;
|
||||
}
|
||||
Assert.state(existingValue == null, () -> "The existing value '" + existingValue +
|
||||
"' of field '" + field + "' is not the same as the new value '" + bean + "'");
|
||||
ReflectionUtils.setField(field, target, bean);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException("Could not inject field '" + field + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,8 +17,7 @@
|
|||
package org.springframework.test.context.bean.override;
|
||||
|
||||
/**
|
||||
* Strategies for bean override instantiation, implemented in
|
||||
* {@link BeanOverrideBeanPostProcessor}.
|
||||
* Strategies for bean override instantiation.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
|
@ -39,10 +38,10 @@ public enum BeanOverrideStrategy {
|
|||
REPLACE_OR_CREATE_DEFINITION,
|
||||
|
||||
/**
|
||||
* Intercept and wrap the actual bean instance upon creation, during the {@linkplain
|
||||
* org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)
|
||||
* early bean reference} phase.
|
||||
* Intercept and process an early bean reference rather than a bean
|
||||
* definition, allowing variants of bean overriding to wrap the instance
|
||||
* (e.g. to delegate to actual methods in the context of a mocking "spy").
|
||||
*/
|
||||
WRAP_EARLY_BEAN
|
||||
WRAP_BEAN
|
||||
|
||||
}
|
||||
|
|
|
@ -56,17 +56,17 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
|
|||
}
|
||||
|
||||
/**
|
||||
* Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata
|
||||
* Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
|
||||
* associated with the current test class and ensure fields are injected
|
||||
* with the overridden bean instance.
|
||||
*/
|
||||
protected void injectFields(TestContext testContext) {
|
||||
postProcessFields(testContext, (testMetadata, postProcessor) -> postProcessor.inject(
|
||||
testMetadata.overrideMetadata.field(), testMetadata.testInstance, testMetadata.overrideMetadata));
|
||||
postProcessFields(testContext, (testMetadata, overrideRegistrar) -> overrideRegistrar.inject(
|
||||
testMetadata.testInstance, testMetadata.overrideMetadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata
|
||||
* Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
|
||||
* associated with the current test class and ensure fields are nulled out
|
||||
* and then re-injected with the overridden bean instance.
|
||||
* <p>This method does nothing if the
|
||||
|
@ -79,31 +79,28 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
|
|||
|
||||
postProcessFields(testContext, (testMetadata, postProcessor) -> {
|
||||
Object testInstance = testMetadata.testInstance;
|
||||
Field field = testMetadata.overrideMetadata.field();
|
||||
Field field = testMetadata.overrideMetadata.getField();
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
ReflectionUtils.setField(field, testInstance, null);
|
||||
postProcessor.inject(field, testInstance, testMetadata.overrideMetadata);
|
||||
postProcessor.inject(testInstance, testMetadata.overrideMetadata);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void postProcessFields(TestContext testContext, BiConsumer<TestContextOverrideMetadata,
|
||||
BeanOverrideBeanPostProcessor> consumer) {
|
||||
BeanOverrideRegistrar> consumer) {
|
||||
|
||||
Class<?> testClass = testContext.getTestClass();
|
||||
Object testInstance = testContext.getTestInstance();
|
||||
BeanOverrideParser parser = new BeanOverrideParser();
|
||||
|
||||
// Avoid full parsing, but validate that this particular class has some bean override field(s).
|
||||
if (parser.hasBeanOverride(testClass)) {
|
||||
BeanOverrideBeanPostProcessor postProcessor =
|
||||
testContext.getApplicationContext().getBean(BeanOverrideBeanPostProcessor.class);
|
||||
// The class should have already been parsed by the context customizer.
|
||||
for (OverrideMetadata metadata : postProcessor.getOverrideMetadata()) {
|
||||
if (!metadata.field().getDeclaringClass().equals(testClass)) {
|
||||
if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) {
|
||||
BeanOverrideRegistrar registrar =
|
||||
testContext.getApplicationContext().getBean(BeanOverrideRegistrar.class);
|
||||
for (OverrideMetadata metadata : registrar.getOverrideMetadata()) {
|
||||
if (!metadata.getField().getDeclaringClass().isAssignableFrom(testClass)) {
|
||||
continue;
|
||||
}
|
||||
consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), postProcessor);
|
||||
consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), registrar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -27,7 +26,16 @@ import org.springframework.core.style.ToStringCreator;
|
|||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Metadata for Bean Overrides.
|
||||
* Metadata for Bean Override injection points, also responsible for the
|
||||
* creation of the overriding instance.
|
||||
*
|
||||
* <p><strong>WARNING</strong>: implementations are used as a cache key and
|
||||
* must implement proper {@code equals} and {@code hashCode}t methods.
|
||||
*
|
||||
* <p>Specific implementations of metadata can have state to be used during
|
||||
* override {@linkplain #createOverride(String, BeanDefinition, Object)
|
||||
* instance creation} — for example, from further parsing of the
|
||||
* annotation or the annotated field.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
|
@ -36,66 +44,45 @@ public abstract class OverrideMetadata {
|
|||
|
||||
private final Field field;
|
||||
|
||||
private final Annotation overrideAnnotation;
|
||||
|
||||
private final ResolvableType typeToOverride;
|
||||
private final ResolvableType beanType;
|
||||
|
||||
private final BeanOverrideStrategy strategy;
|
||||
|
||||
|
||||
protected OverrideMetadata(Field field, Annotation overrideAnnotation,
|
||||
ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
|
||||
protected OverrideMetadata(Field field, ResolvableType beanType,
|
||||
BeanOverrideStrategy strategy) {
|
||||
|
||||
this.field = field;
|
||||
this.overrideAnnotation = overrideAnnotation;
|
||||
this.typeToOverride = typeToOverride;
|
||||
this.beanType = beanType;
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a short, human-readable description of the kind of override this
|
||||
* instance handles.
|
||||
* Return the bean name to override.
|
||||
*/
|
||||
public abstract String getBeanOverrideDescription();
|
||||
|
||||
/**
|
||||
* Return the expected bean name to override.
|
||||
* <p>Typically, this is either explicitly set in a concrete annotation or
|
||||
* inferred from the annotated field's name.
|
||||
* @return the expected bean name
|
||||
*/
|
||||
protected String getExpectedBeanName() {
|
||||
protected String getBeanName() {
|
||||
return this.field.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the annotated {@link Field}.
|
||||
*/
|
||||
public Field field() {
|
||||
return this.field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the concrete override annotation, that is the one meta-annotated
|
||||
* with {@link BeanOverride @BeanOverride}.
|
||||
*/
|
||||
public Annotation overrideAnnotation() {
|
||||
return this.overrideAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean {@link ResolvableType type} to override.
|
||||
*/
|
||||
public ResolvableType typeToOverride() {
|
||||
return this.typeToOverride;
|
||||
public ResolvableType getBeanType() {
|
||||
return this.beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the annotated {@link Field}.
|
||||
*/
|
||||
public Field getField() {
|
||||
return this.field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link BeanOverrideStrategy} for this instance, as a hint on
|
||||
* how and when the override instance should be created.
|
||||
*/
|
||||
public final BeanOverrideStrategy getBeanOverrideStrategy() {
|
||||
public BeanOverrideStrategy getStrategy() {
|
||||
return this.strategy;
|
||||
}
|
||||
|
||||
|
@ -133,25 +120,22 @@ public abstract class OverrideMetadata {
|
|||
return false;
|
||||
}
|
||||
OverrideMetadata that = (OverrideMetadata) obj;
|
||||
return Objects.equals(this.field, that.field) &&
|
||||
Objects.equals(this.overrideAnnotation, that.overrideAnnotation) &&
|
||||
Objects.equals(this.strategy, that.strategy) &&
|
||||
Objects.equals(typeToOverride(), that.typeToOverride());
|
||||
return Objects.equals(this.strategy, that.strategy) &&
|
||||
Objects.equals(this.field, that.field) &&
|
||||
Objects.equals(this.beanType, that.beanType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.field, this.overrideAnnotation, this.strategy, typeToOverride());
|
||||
return Objects.hash(this.strategy, this.field, this.beanType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("category", getBeanOverrideDescription())
|
||||
.append("field", this.field)
|
||||
.append("overrideAnnotation", this.overrideAnnotation)
|
||||
.append("strategy", this.strategy)
|
||||
.append("typeToOverride", typeToOverride())
|
||||
.append("field", this.field)
|
||||
.append("beanType", this.beanType)
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,9 +28,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
|||
/**
|
||||
* Mark a field to override a bean instance in the {@code BeanFactory}.
|
||||
*
|
||||
* <p>The instance is created from a zero-argument static factory method in the test
|
||||
* class whose return type is compatible with the annotated field. The method
|
||||
* is deduced as follows.
|
||||
* <p>The instance is created from a zero-argument static factory method in the
|
||||
* test class whose return type is compatible with the annotated field. In the
|
||||
* case of a nested test, any enclosing class it might have is also considered.
|
||||
* Similarly, in case the test class inherits from a base class the whole class
|
||||
* hierarchy is considered. The method is deduced as follows.
|
||||
* <ul>
|
||||
* <li>If the {@link #methodName()} is specified, look for a static method with
|
||||
* that name.</li>
|
||||
|
@ -121,6 +123,9 @@ public @interface TestBean {
|
|||
/**
|
||||
* Name of a static factory method to look for in the test class, which will
|
||||
* be used to instantiate the bean to override.
|
||||
* <p>In the case of a nested test, any enclosing class it might have is
|
||||
* also considered. Similarly, in case the test class inherits from a base
|
||||
* class the whole class hierarchy is considered.
|
||||
* <p>If left unspecified, the name of the factory method will be detected
|
||||
* based on convention.
|
||||
* @see #CONVENTION_SUFFIX
|
||||
|
|
|
@ -24,11 +24,13 @@ import java.lang.reflect.Modifier;
|
|||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
|
||||
import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||
|
@ -37,126 +39,155 @@ import org.springframework.util.ReflectionUtils;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link BeanOverrideProcessor} implementation primarily made to work with
|
||||
* fields annotated with {@link TestBean @TestBean}, but can also work with
|
||||
* arbitrary test bean override annotations provided the annotated field's
|
||||
* declaring class declares an appropriate test bean factory method according
|
||||
* to the conventions documented in {@link TestBean}.
|
||||
* {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
|
||||
* support. It creates metadata for annotated fields in a given class and
|
||||
* ensures that a corresponding static factory method exists, according to the
|
||||
* {@linkplain TestBean documented conventions}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
class TestBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
/**
|
||||
* Find a test bean factory {@link Method} in the given {@link Class} which
|
||||
* meets the following criteria.
|
||||
* Find a test bean factory {@link Method} in the given {@link Class} or one
|
||||
* of its parent classes, which meets the following criteria.
|
||||
* <ul>
|
||||
* <li>The method is static.</li>
|
||||
* <li>The method does not accept any arguments.</li>
|
||||
* <li>The method's return type matches the supplied {@code methodReturnType}.</li>
|
||||
* <li>The method's name is one of the supplied {@code methodNames}.</li>
|
||||
* </ul>
|
||||
* <p>If the test class inherits from another class, the class hierarchy is
|
||||
* searched for factory methods. Matching factory methods are prioritized
|
||||
* from closest to furthest from the test class in the class hierarchy,
|
||||
* provided they have the same name. However, if multiple methods are found
|
||||
* that match distinct candidate names, an exception is thrown.
|
||||
* @param clazz the class in which to search for the factory method
|
||||
* @param methodReturnType the return type for the factory method
|
||||
* @param methodNames a set of supported names for the factory method
|
||||
* @return the corresponding factory method
|
||||
* @throws IllegalStateException if a single matching factory method cannot
|
||||
* be found
|
||||
* @throws IllegalStateException if a matching factory method cannot
|
||||
* be found or multiple methods have a match
|
||||
*/
|
||||
public static Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
|
||||
static Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
|
||||
Assert.isTrue(methodNames.length > 0, "At least one candidate method name is required");
|
||||
Set<String> supportedNames = new LinkedHashSet<>(Arrays.asList(methodNames));
|
||||
List<Method> methods = Arrays.stream(clazz.getDeclaredMethods())
|
||||
List<Method> methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz))
|
||||
.filter(method -> Modifier.isStatic(method.getModifiers()) &&
|
||||
supportedNames.contains(method.getName()) &&
|
||||
methodReturnType.isAssignableFrom(method.getReturnType()))
|
||||
.toList();
|
||||
|
||||
if (methods.isEmpty() && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
|
||||
methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz.getEnclosingClass()))
|
||||
.filter(method -> Modifier.isStatic(method.getModifiers()) &&
|
||||
supportedNames.contains(method.getName()) &&
|
||||
methodReturnType.isAssignableFrom(method.getReturnType()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Assert.state(!methods.isEmpty(), () -> """
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""".formatted(
|
||||
clazz.getName(), methodReturnType.getName(), supportedNames));
|
||||
|
||||
Assert.state(methods.size() == 1, () -> """
|
||||
long nameCount = methods.stream().map(Method::getName).distinct().count();
|
||||
int methodCount = methods.size();
|
||||
Assert.state(nameCount == 1, () -> """
|
||||
Found %d competing static test bean factory methods in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""".formatted(
|
||||
methods.size(), clazz.getName(), methodReturnType.getName(), supportedNames));
|
||||
methodCount, clazz.getName(), methodReturnType.getName(), supportedNames));
|
||||
|
||||
return methods.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
// If we can, get an explicit method name right away; fail fast if it doesn't match.
|
||||
if (overrideAnnotation instanceof TestBean testBeanAnnotation) {
|
||||
Method overrideMethod = null;
|
||||
String beanName = null;
|
||||
if (!testBeanAnnotation.methodName().isBlank()) {
|
||||
Class<?> declaringClass = field.getDeclaringClass();
|
||||
overrideMethod = findTestBeanFactoryMethod(declaringClass, field.getType(), testBeanAnnotation.methodName());
|
||||
}
|
||||
if (!testBeanAnnotation.name().isBlank()) {
|
||||
beanName = testBeanAnnotation.name();
|
||||
}
|
||||
return new MethodConventionOverrideMetadata(field, overrideMethod, beanName,
|
||||
overrideAnnotation, typeToOverride);
|
||||
public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (!(overrideAnnotation instanceof TestBean testBeanAnnotation)) {
|
||||
throw new IllegalStateException(String.format("Invalid annotation passed to %s: expected @TestBean on field %s.%s",
|
||||
TestBeanOverrideProcessor.class.getSimpleName(), field.getDeclaringClass().getName(),
|
||||
field.getName()));
|
||||
}
|
||||
// Otherwise defer the resolution of the static method until OverrideMetadata#createOverride.
|
||||
return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, typeToOverride);
|
||||
// If the user specified a method explicitly, search for that.
|
||||
// Otherwise, search candidate factory methods using the convention suffix
|
||||
// and the explicit bean name (if any) or field name.
|
||||
Method explicitOverrideMethod;
|
||||
if (!testBeanAnnotation.methodName().isBlank()) {
|
||||
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), testBeanAnnotation.methodName());
|
||||
}
|
||||
else {
|
||||
String beanName = testBeanAnnotation.name();
|
||||
if (!StringUtils.hasText(beanName)) {
|
||||
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
|
||||
field.getName() + TestBean.CONVENTION_SUFFIX);
|
||||
}
|
||||
else {
|
||||
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
|
||||
beanName + TestBean.CONVENTION_SUFFIX,
|
||||
field.getName() + TestBean.CONVENTION_SUFFIX);
|
||||
}
|
||||
}
|
||||
|
||||
return new TestBeanOverrideMetadata(field, explicitOverrideMethod, testBeanAnnotation, ResolvableType.forField(field, testClass));
|
||||
}
|
||||
|
||||
|
||||
static final class MethodConventionOverrideMetadata extends OverrideMetadata {
|
||||
static final class TestBeanOverrideMetadata extends OverrideMetadata {
|
||||
|
||||
@Nullable
|
||||
private final Method overrideMethod;
|
||||
|
||||
@Nullable
|
||||
private final String beanName;
|
||||
|
||||
public MethodConventionOverrideMetadata(Field field, @Nullable Method overrideMethod, @Nullable String beanName,
|
||||
Annotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
public TestBeanOverrideMetadata(Field field, Method overrideMethod, TestBean overrideAnnotation,
|
||||
ResolvableType typeToOverride) {
|
||||
|
||||
super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
this.beanName = StringUtils.hasText(overrideAnnotation.name()) ?
|
||||
overrideAnnotation.name() : field.getName();
|
||||
this.overrideMethod = overrideMethod;
|
||||
this.beanName = beanName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getExpectedBeanName() {
|
||||
if (StringUtils.hasText(this.beanName)) {
|
||||
return this.beanName;
|
||||
}
|
||||
return super.getExpectedBeanName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return "@TestBean";
|
||||
protected String getBeanName() {
|
||||
return this.beanName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
|
||||
@Nullable Object existingBeanInstance) {
|
||||
|
||||
Method methodToInvoke = this.overrideMethod;
|
||||
if (methodToInvoke == null) {
|
||||
methodToInvoke = findTestBeanFactoryMethod(field().getDeclaringClass(), field().getType(),
|
||||
beanName + TestBean.CONVENTION_SUFFIX,
|
||||
field().getName() + TestBean.CONVENTION_SUFFIX);
|
||||
}
|
||||
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(methodToInvoke);
|
||||
return methodToInvoke.invoke(null);
|
||||
ReflectionUtils.makeAccessible(this.overrideMethod);
|
||||
return this.overrideMethod.invoke(null);
|
||||
}
|
||||
catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() +
|
||||
throw new IllegalArgumentException("Could not invoke bean overriding method " + this.overrideMethod.getName() +
|
||||
"; a static method with no formal parameters is expected", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
TestBeanOverrideMetadata that = (TestBeanOverrideMetadata) o;
|
||||
return Objects.equals(this.overrideMethod, that.overrideMethod)
|
||||
&& Objects.equals(this.beanName, that.beanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), this.overrideMethod, this.beanName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.mockito.Answers;
|
||||
|
@ -44,7 +44,7 @@ import static org.mockito.Mockito.mock;
|
|||
* @author Phillip Webb
|
||||
* @since 6.2
|
||||
*/
|
||||
class MockDefinition extends Definition {
|
||||
class MockitoBeanMetadata extends MockitoMetadata {
|
||||
|
||||
private final Set<Class<?>> extraInterfaces;
|
||||
|
||||
|
@ -53,33 +53,27 @@ class MockDefinition extends Definition {
|
|||
private final boolean serializable;
|
||||
|
||||
|
||||
MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) {
|
||||
this(annotation.name(), annotation.reset(), field, annotation, typeToMock,
|
||||
MockitoBeanMetadata(MockitoBean annotation, Field field, ResolvableType typeToMock) {
|
||||
this(annotation.name(), annotation.reset(), field, typeToMock,
|
||||
annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
|
||||
}
|
||||
|
||||
MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock,
|
||||
MockitoBeanMetadata(String name, MockReset reset, Field field, ResolvableType typeToMock,
|
||||
Class<?>[] extraInterfaces, @Nullable Answers answer, boolean serializable) {
|
||||
|
||||
super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
|
||||
super(name, reset, false, field, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
|
||||
Assert.notNull(typeToMock, "TypeToMock must not be null");
|
||||
this.extraInterfaces = asClassSet(extraInterfaces);
|
||||
this.answer = (answer != null) ? answer : Answers.RETURNS_DEFAULTS;
|
||||
this.serializable = serializable;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return "@MockitoBean";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
|
||||
return createMock(beanName);
|
||||
}
|
||||
|
||||
private Set<Class<?>> asClassSet(Class<?>[] classes) {
|
||||
private Set<Class<?>> asClassSet(@Nullable Class<?>[] classes) {
|
||||
Set<Class<?>> classSet = new LinkedHashSet<>();
|
||||
if (classes != null) {
|
||||
classSet.addAll(Arrays.asList(classes));
|
||||
|
@ -96,8 +90,8 @@ class MockDefinition extends Definition {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the answers mode.
|
||||
* @return the answers mode; never {@code null}
|
||||
* Return the {@link Answers}.
|
||||
* @return the answers mode
|
||||
*/
|
||||
Answers getAnswer() {
|
||||
return this.answer;
|
||||
|
@ -119,9 +113,8 @@ class MockDefinition extends Definition {
|
|||
if (obj == null || obj.getClass() != getClass()) {
|
||||
return false;
|
||||
}
|
||||
MockDefinition other = (MockDefinition) obj;
|
||||
MockitoBeanMetadata other = (MockitoBeanMetadata) obj;
|
||||
boolean result = super.equals(obj);
|
||||
result = result && ObjectUtils.nullSafeEquals(typeToOverride(), other.typeToOverride());
|
||||
result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer);
|
||||
result = result && this.serializable == other.serializable;
|
||||
|
@ -130,30 +123,21 @@ class MockDefinition extends Definition {
|
|||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces);
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer);
|
||||
result = MULTIPLIER * result + Boolean.hashCode(this.serializable);
|
||||
return result;
|
||||
return Objects.hash(super.hashCode(), this.extraInterfaces, this.answer, this.serializable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("name", this.name)
|
||||
.append("typeToMock", typeToOverride())
|
||||
.append("extraInterfaces", this.extraInterfaces)
|
||||
.append("answer", this.answer)
|
||||
.append("serializable", this.serializable)
|
||||
.append("beanName", getBeanName())
|
||||
.append("fieldType", getBeanType())
|
||||
.append("extraInterfaces", getExtraInterfaces())
|
||||
.append("answer", getAnswer())
|
||||
.append("serializable", isSerializable())
|
||||
.append("reset", getReset())
|
||||
.toString();
|
||||
}
|
||||
|
||||
<T> T createMock() {
|
||||
return createMock(this.name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> T createMock(String name) {
|
||||
MockSettings settings = MockReset.withSettings(getReset());
|
||||
|
@ -167,7 +151,7 @@ class MockDefinition extends Definition {
|
|||
if (this.serializable) {
|
||||
settings.serializable();
|
||||
}
|
||||
return (T) mock(typeToOverride().resolve(), settings);
|
||||
return (T) mock(getBeanType().resolve(), settings);
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,6 @@ import java.lang.reflect.Field;
|
|||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
|
||||
import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||
|
||||
/**
|
||||
* A {@link BeanOverrideProcessor} for mockito-related annotations
|
||||
|
@ -30,18 +29,19 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
|
|||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
*/
|
||||
public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
@Override
|
||||
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) {
|
||||
public MockitoMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (overrideAnnotation instanceof MockitoBean mockBean) {
|
||||
return new MockDefinition(mockBean, field, typeToMock);
|
||||
return new MockitoBeanMetadata(mockBean, field, ResolvableType.forField(field, testClass));
|
||||
}
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
|
||||
return new SpyDefinition(spyBean, field, typeToMock);
|
||||
return new MockitoSpyBeanMetadata(spyBean, field, ResolvableType.forField(field, testClass));
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " +
|
||||
overrideAnnotation.getClass().getName());
|
||||
throw new IllegalStateException(String.format("Invalid annotation passed to MockitoBeanOverrideProcessor: "
|
||||
+ "expected @MockitoBean/@MockitoSpyBean on field %s.%s",
|
||||
field.getDeclaringClass().getName(), field.getName()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.SingletonBeanRegistry;
|
||||
|
@ -29,15 +29,12 @@ import org.springframework.util.ObjectUtils;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for {@link MockDefinition} and {@link SpyDefinition}.
|
||||
* Base class for {@link MockitoBeanMetadata} and {@link MockitoSpyBeanMetadata}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 6.2
|
||||
*/
|
||||
abstract class Definition extends OverrideMetadata {
|
||||
|
||||
protected static final int MULTIPLIER = 31;
|
||||
|
||||
abstract class MockitoMetadata extends OverrideMetadata {
|
||||
|
||||
protected final String name;
|
||||
|
||||
|
@ -46,10 +43,10 @@ abstract class Definition extends OverrideMetadata {
|
|||
private final boolean proxyTargetAware;
|
||||
|
||||
|
||||
Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
|
||||
Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
|
||||
MockitoMetadata(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
|
||||
ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
|
||||
|
||||
super(field, annotation, typeToOverride, strategy);
|
||||
super(field, typeToOverride, strategy);
|
||||
this.name = name;
|
||||
this.reset = (reset != null) ? reset : MockReset.AFTER;
|
||||
this.proxyTargetAware = proxyTargetAware;
|
||||
|
@ -57,11 +54,11 @@ abstract class Definition extends OverrideMetadata {
|
|||
|
||||
|
||||
@Override
|
||||
protected String getExpectedBeanName() {
|
||||
protected String getBeanName() {
|
||||
if (StringUtils.hasText(this.name)) {
|
||||
return this.name;
|
||||
}
|
||||
return super.getExpectedBeanName();
|
||||
return super.getBeanName();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -104,8 +101,9 @@ abstract class Definition extends OverrideMetadata {
|
|||
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
|
||||
return false;
|
||||
}
|
||||
Definition other = (Definition) obj;
|
||||
boolean result = ObjectUtils.nullSafeEquals(this.name, other.name);
|
||||
MockitoMetadata other = (MockitoMetadata) obj;
|
||||
boolean result = super.equals(obj);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.name, other.name);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.reset, other.reset);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.proxyTargetAware, other.proxyTargetAware);
|
||||
return result;
|
||||
|
@ -113,11 +111,7 @@ abstract class Definition extends OverrideMetadata {
|
|||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 1;
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
|
||||
return result;
|
||||
return Objects.hash(super.hashCode(), this.name, this.reset, this.proxyTargetAware);
|
||||
}
|
||||
|
||||
}
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.mockito.AdditionalAnswers;
|
||||
import org.mockito.MockSettings;
|
||||
|
@ -44,26 +44,20 @@ import static org.mockito.Mockito.mock;
|
|||
* @author Phillip Webb
|
||||
* @since 6.2
|
||||
*/
|
||||
class SpyDefinition extends Definition {
|
||||
class MockitoSpyBeanMetadata extends MockitoMetadata {
|
||||
|
||||
SpyDefinition(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
|
||||
this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(), field,
|
||||
spyAnnotation, typeToSpy);
|
||||
MockitoSpyBeanMetadata(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
|
||||
this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(),
|
||||
field, typeToSpy);
|
||||
}
|
||||
|
||||
SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation,
|
||||
ResolvableType typeToSpy) {
|
||||
MockitoSpyBeanMetadata(String name, MockReset reset, boolean proxyTargetAware, Field field, ResolvableType typeToSpy) {
|
||||
|
||||
super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN);
|
||||
super(name, reset, proxyTargetAware, field, typeToSpy, BeanOverrideStrategy.WRAP_BEAN);
|
||||
Assert.notNull(typeToSpy, "typeToSpy must not be null");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return "@MockitoSpyBean";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
|
||||
@Nullable Object existingBeanInstance) {
|
||||
|
@ -75,41 +69,35 @@ class SpyDefinition extends Definition {
|
|||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
// For SpyBean we want the class to be exactly the same.
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
// For SpyBean we want the class to be exactly the same.
|
||||
if (obj == null || obj.getClass() != getClass()) {
|
||||
return false;
|
||||
}
|
||||
SpyDefinition that = (SpyDefinition) obj;
|
||||
return (super.equals(obj) && ObjectUtils.nullSafeEquals(typeToOverride(), that.typeToOverride()));
|
||||
MockitoSpyBeanMetadata that = (MockitoSpyBeanMetadata) obj;
|
||||
return (super.equals(obj) && ObjectUtils.nullSafeEquals(getBeanType(), that.getBeanType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
|
||||
return result;
|
||||
return Objects.hash(super.hashCode(), getBeanType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("name", this.name)
|
||||
.append("typeToSpy", typeToOverride())
|
||||
.append("beanName", getBeanName())
|
||||
.append("beanType", getBeanType())
|
||||
.append("reset", getReset())
|
||||
.toString();
|
||||
}
|
||||
|
||||
<T> T createSpy(Object instance) {
|
||||
return createSpy(this.name, instance);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> T createSpy(String name, Object instance) {
|
||||
Assert.notNull(instance, "Instance must not be null");
|
||||
Class<?> resolvedTypeToOverride = typeToOverride().resolve();
|
||||
Class<?> resolvedTypeToOverride = getBeanType().resolve();
|
||||
Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override");
|
||||
Assert.isInstanceOf(resolvedTypeToOverride, instance);
|
||||
if (Mockito.mockingDetails(instance).isSpy()) {
|
||||
|
@ -125,7 +113,7 @@ class SpyDefinition extends Definition {
|
|||
Class<?> toSpy;
|
||||
if (Proxy.isProxyClass(instance.getClass())) {
|
||||
settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance));
|
||||
toSpy = typeToOverride().toClass();
|
||||
toSpy = getBeanType().toClass();
|
||||
}
|
||||
else {
|
||||
settings.defaultAnswer(Mockito.CALLS_REAL_METHODS);
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -46,20 +47,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
|||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for for {@link BeanOverrideBeanPostProcessor}.
|
||||
* Tests for {@link BeanOverrideBeanFactoryPostProcessor} combined with a
|
||||
* {@link BeanOverrideRegistrar}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
private final BeanOverrideParser parser = new BeanOverrideParser();
|
||||
|
||||
class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void canReplaceExistingBeanDefinitions() {
|
||||
this.parser.parse(ReplaceBeans.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
|
||||
context.register(ReplaceBeans.class);
|
||||
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
|
@ -72,22 +69,19 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void cannotReplaceIfNoBeanMatching() {
|
||||
this.parser.parse(ReplaceBeans.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
|
||||
context.register(ReplaceBeans.class);
|
||||
//note we don't register any original bean here
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'");
|
||||
.withMessage("Unable to override bean 'explicit'; " +
|
||||
"there is no bean definition to replace with that name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() {
|
||||
this.parser.parse(CreateIfOriginalIsMissingBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
|
||||
context.register(CreateIfOriginalIsMissingBean.class);
|
||||
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
|
@ -100,9 +94,7 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void canCreateIfOriginalMissingWithCreateReplaceStrategy() {
|
||||
this.parser.parse(CreateIfOriginalIsMissingBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
|
||||
context.register(CreateIfOriginalIsMissingBean.class);
|
||||
//note we don't register original beans here
|
||||
|
||||
|
@ -114,9 +106,7 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() {
|
||||
this.parser.parse(OverriddenFactoryBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
|
||||
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class);
|
||||
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
|
||||
|
@ -129,9 +119,7 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() {
|
||||
this.parser.parse(OverriddenFactoryBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
|
||||
ResolvableType objectType = ResolvableType.forClass(SomeInterface.class);
|
||||
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType);
|
||||
|
@ -145,10 +133,9 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void postProcessorShouldNotTriggerEarlyInitialization() {
|
||||
this.parser.parse(EagerInitBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(EagerInitBean.class);
|
||||
|
||||
context.register(FactoryBeanRegisteringPostProcessor.class);
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
context.register(EarlyBeanInitializationDetector.class);
|
||||
context.register(EagerInitBean.class);
|
||||
|
||||
|
@ -157,12 +144,10 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void allowReplaceDefinitionWhenSingletonDefinitionPresent() {
|
||||
this.parser.parse(SingletonBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
|
||||
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
|
||||
definition.setScope(BeanDefinition.SCOPE_SINGLETON);
|
||||
context.registerBeanDefinition("singleton", definition);
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
context.register(SingletonBean.class);
|
||||
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
|
@ -172,14 +157,12 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
|
||||
@Test
|
||||
void copyDefinitionPrimaryAndScope() {
|
||||
this.parser.parse(SingletonBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
|
||||
context.getBeanFactory().registerScope("customScope", new SimpleThreadScope());
|
||||
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
|
||||
definition.setScope("customScope");
|
||||
definition.setPrimary(true);
|
||||
context.registerBeanDefinition("singleton", definition);
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
context.register(SingletonBean.class);
|
||||
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
|
@ -192,6 +175,14 @@ class BeanOverrideBeanPostProcessorTests {
|
|||
}
|
||||
|
||||
|
||||
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideRegistrar.register(context, Set.of(classes));
|
||||
BeanOverrideBeanFactoryPostProcessor.register(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Classes to parse and register with the bean post processor
|
||||
-----
|
|
@ -28,33 +28,27 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BeanOverrideParser}.
|
||||
* Unit tests for {@link BeanOverrideParsingUtils}.
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
class BeanOverrideParserTests {
|
||||
|
||||
// Copy of ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER which is package-private.
|
||||
private static final String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
private final BeanOverrideParser parser = new BeanOverrideParser();
|
||||
class BeanOverrideParsingUtilsTests {
|
||||
|
||||
// Metadata built from a String that starts with DUPLICATE_TRIGGER are considered equal
|
||||
private static final String DUPLICATE_TRIGGER1 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v1";
|
||||
private static final String DUPLICATE_TRIGGER2 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v2";
|
||||
|
||||
@Test
|
||||
void findsOnField() {
|
||||
parser.parse(SingleAnnotationOnField.class);
|
||||
|
||||
assertThat(parser.getOverrideMetadata())
|
||||
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
|
||||
assertThat(BeanOverrideParsingUtils.parse(SingleAnnotationOnField.class))
|
||||
.map(Object::toString)
|
||||
.containsExactly("onField");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsMultipleProcessorsOnDifferentElements() {
|
||||
parser.parse(AnnotationsOnMultipleFields.class);
|
||||
|
||||
assertThat(parser.getOverrideMetadata())
|
||||
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
|
||||
assertThat(BeanOverrideParsingUtils.parse(AnnotationsOnMultipleFields.class))
|
||||
.map(Object::toString)
|
||||
.containsExactlyInAnyOrder("onField1", "onField2");
|
||||
}
|
||||
|
||||
|
@ -62,15 +56,15 @@ class BeanOverrideParserTests {
|
|||
void rejectsMultipleAnnotationsOnSameElement() {
|
||||
Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message");
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class))
|
||||
.isThrownBy(() -> BeanOverrideParsingUtils.parse(MultipleAnnotationsOnField.class))
|
||||
.withMessage("Multiple @BeanOverride annotations found on field: " + field);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsDuplicateMetadata() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> parser.parse(DuplicateConf.class))
|
||||
.withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}");
|
||||
void keepsFirstOccurrenceOfEqualMetadata() {
|
||||
assertThat(BeanOverrideParsingUtils.parse(DuplicateConf.class))
|
||||
.map(Object::toString)
|
||||
.containsExactly("{DUPLICATE-v1}");
|
||||
}
|
||||
|
||||
|
||||
|
@ -114,10 +108,10 @@ class BeanOverrideParserTests {
|
|||
|
||||
static class DuplicateConf {
|
||||
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER1)
|
||||
String message1;
|
||||
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER2)
|
||||
String message2;
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -39,7 +38,7 @@ class OverrideMetadataTests {
|
|||
@Test
|
||||
void implicitConfigurations() throws Exception {
|
||||
OverrideMetadata metadata = exampleOverride();
|
||||
assertThat(metadata.getExpectedBeanName()).as("expectedBeanName").isEqualTo(metadata.field().getName());
|
||||
assertThat(metadata.getBeanName()).as("expectedBeanName").isEqualTo(metadata.getField().getName());
|
||||
}
|
||||
|
||||
|
||||
|
@ -48,21 +47,16 @@ class OverrideMetadataTests {
|
|||
|
||||
private static OverrideMetadata exampleOverride() throws Exception {
|
||||
Field field = OverrideMetadataTests.class.getDeclaredField("annotated");
|
||||
return new ConcreteOverrideMetadata(field, field.getAnnotation(NonNull.class),
|
||||
ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
return new ConcreteOverrideMetadata(field, ResolvableType.forClass(String.class),
|
||||
BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
}
|
||||
|
||||
static class ConcreteOverrideMetadata extends OverrideMetadata {
|
||||
|
||||
ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride,
|
||||
ConcreteOverrideMetadata(Field field, ResolvableType typeToOverride,
|
||||
BeanOverrideStrategy strategy) {
|
||||
|
||||
super(field, overrideAnnotation, typeToOverride, strategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return ConcreteOverrideMetadata.class.getSimpleName();
|
||||
super(field, typeToOverride, strategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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.convention;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@SpringJUnitConfig
|
||||
class AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@TestBean
|
||||
Pojo someBean;
|
||||
|
||||
@TestBean
|
||||
Pojo otherBean;
|
||||
|
||||
@TestBean(name = "thirdBean")
|
||||
Pojo anotherBean;
|
||||
|
||||
static Pojo otherBeanTestOverride() {
|
||||
return new FakePojo("otherBean in superclass");
|
||||
}
|
||||
|
||||
static Pojo thirdBeanTestOverride() {
|
||||
return new FakePojo("third in superclass");
|
||||
}
|
||||
|
||||
static Pojo commonBeanOverride() {
|
||||
return new FakePojo("in superclass");
|
||||
}
|
||||
|
||||
interface Pojo {
|
||||
|
||||
default String getValue() {
|
||||
return "Prod";
|
||||
}
|
||||
}
|
||||
|
||||
static class ProdPojo implements Pojo { }
|
||||
|
||||
static class FakePojo implements Pojo {
|
||||
final String value;
|
||||
|
||||
protected FakePojo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
Pojo someBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo otherBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo thirdBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo2() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
public class TestBeanInheritanceIntegrationTests {
|
||||
|
||||
static AbstractTestBeanIntegrationTestCase.Pojo nestedBeanOverride() {
|
||||
return new AbstractTestBeanIntegrationTestCase.FakePojo("in enclosing test class");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Concrete inherited test with correct @TestBean setup")
|
||||
class ConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@TestBean(methodName = "commonBeanOverride")
|
||||
Pojo pojo;
|
||||
|
||||
@TestBean(methodName = "nestedBeanOverride")
|
||||
Pojo pojo2;
|
||||
|
||||
static Pojo someBeanTestOverride() {
|
||||
return new FakePojo("someBeanOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypeMethodInType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
|
||||
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInTypeMethodInSuperType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
|
||||
assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInTypeMethodInEnclosingClass(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
|
||||
assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypePrioritizeMethodInType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
|
||||
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void failsIfFieldInSupertypeButNoMethod() {
|
||||
Class<?> clazz = Failing1.class;
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(clazz))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates [someBeanTestOverride]""",
|
||||
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsIfMethod1InSupertypeAndMethod2InType() {
|
||||
Class<?> clazz = Failing2.class;
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(clazz))
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("""
|
||||
Found 2 competing static test bean factory methods in %s with return type %s \
|
||||
whose name matches one of the supported candidates \
|
||||
[thirdBeanTestOverride, anotherBeanTestOverride]""",
|
||||
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
|
||||
}
|
||||
|
||||
static class Failing1 extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
}
|
||||
}
|
||||
|
||||
static class Failing2 extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
static Pojo someBeanTestOverride() {
|
||||
return new FakePojo("ignored");
|
||||
}
|
||||
|
||||
static Pojo anotherBeanTestOverride() {
|
||||
return new FakePojo("sub2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignored2() { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
@SpringJUnitConfig
|
||||
public class TestBeanIntegrationTests {
|
||||
|
||||
@TestBean
|
||||
String field;
|
||||
|
||||
@TestBean
|
||||
String nestedField;
|
||||
|
||||
@TestBean(name = "field")
|
||||
String renamed1;
|
||||
|
||||
@TestBean(name = "nestedField")
|
||||
String renamed2;
|
||||
|
||||
@TestBean(methodName = "fieldTestOverride")
|
||||
String methodRenamed1;
|
||||
|
||||
@TestBean(methodName = "nestedFieldTestOverride")
|
||||
String methodRenamed2;
|
||||
|
||||
static String fieldTestOverride() {
|
||||
return "fieldOverride";
|
||||
}
|
||||
|
||||
static String nestedFieldTestOverride() {
|
||||
return "nestedFieldOverride";
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.field).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.methodRenamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoFieldNameBean() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing1.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'noOriginalBean'; " +
|
||||
"there is no bean definition to replace with that name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoExplicitNameBean() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing2.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresent'; " +
|
||||
"there is no bean definition to replace with that name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoImplicitMethod() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing3.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Failed to find a static test bean factory method in " +
|
||||
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing3 " +
|
||||
"with return type java.lang.String whose name matches one of the " +
|
||||
"supported candidates [notPresent]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoExplicitMethod() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing4.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Failed to find a static test bean factory method in " +
|
||||
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing4 " +
|
||||
"with return type java.lang.String whose name matches one of the " +
|
||||
"supported candidates [fieldTestOverride]"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @TestBean on enclosing class")
|
||||
class TestBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.nestedField).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.renamed2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.methodRenamed2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With factory method on enclosing class")
|
||||
class TestBeanNested2 {
|
||||
|
||||
@TestBean(methodName = "nestedFieldTestOverride", name = "nestedField")
|
||||
String nestedField2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(this.nestedField2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean("field")
|
||||
String bean1() {
|
||||
return "prod";
|
||||
}
|
||||
|
||||
@Bean("nestedField")
|
||||
String bean2() {
|
||||
return "nestedProd";
|
||||
}
|
||||
|
||||
@Bean("methodRenamed1")
|
||||
String bean3() {
|
||||
return "Prod";
|
||||
}
|
||||
|
||||
@Bean("methodRenamed2")
|
||||
String bean4() {
|
||||
return "NestedProd";
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing1 {
|
||||
|
||||
@TestBean
|
||||
String noOriginalBean;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
|
||||
static String noOriginalBeanTestOverride() {
|
||||
return "should be ignored";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing2 {
|
||||
|
||||
@TestBean(name = "notPresent")
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
|
||||
static String notPresentTestOverride() {
|
||||
return "should be ignored";
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing3 {
|
||||
|
||||
@TestBean(methodName = "notPresent")
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing4 {
|
||||
|
||||
@TestBean //expects fieldTestOverride method
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,8 +23,9 @@ import java.util.List;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.MethodConventionOverrideMetadata;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.TestBeanOverrideMetadata;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.FailingExampleService;
|
||||
|
||||
|
@ -95,7 +96,7 @@ class TestBeanOverrideProcessorTests {
|
|||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isThrownBy(() -> processor.createMetadata(overrideAnnotation, clazz, field))
|
||||
.withMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""",
|
||||
|
@ -104,35 +105,49 @@ class TestBeanOverrideProcessorTests {
|
|||
|
||||
@Test
|
||||
void createMetaDataForKnownExplicitMethod() throws Exception {
|
||||
Class<?> returnType = ExampleService.class;
|
||||
Field field = ExplicitMethodNameConf.class.getField("b");
|
||||
Class<?> clazz = ExplicitMethodNameConf.class;
|
||||
Field field = clazz.getField("b");
|
||||
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
|
||||
assertThat(overrideAnnotation).isNotNull();
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isInstanceOf(MethodConventionOverrideMetadata.class);
|
||||
assertThat(processor.createMetadata(overrideAnnotation, clazz, field))
|
||||
.isInstanceOf(TestBeanOverrideMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception {
|
||||
void createMetaDataForConventionBasedFactoryMethod() throws Exception {
|
||||
Class<?> returnType = ExampleService.class;
|
||||
Field field = MethodConventionConf.class.getField("field");
|
||||
Class<?> clazz = MethodConventionConf.class;
|
||||
Field field = clazz.getField("field");
|
||||
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
|
||||
assertThat(overrideAnnotation).isNotNull();
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
// When in convention-based mode, createMetadata() will not verify that
|
||||
// the factory method actually exists. So, we don't expect an exception
|
||||
// for this use case.
|
||||
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isInstanceOf(MethodConventionOverrideMetadata.class);
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(
|
||||
overrideAnnotation, clazz, field))
|
||||
.withMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""",
|
||||
clazz.getName(), returnType.getName(), List.of("someFieldTestOverride", "fieldTestOverride"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failToCreateMetadataForOtherAnnotation() throws NoSuchFieldException {
|
||||
Class<?> clazz = MethodConventionConf.class;
|
||||
Field field = clazz.getField("field");
|
||||
NonNull badAnnotation = AnnotationUtils.synthesizeAnnotation(NonNull.class);
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(badAnnotation, clazz, field))
|
||||
.withMessage("Invalid annotation passed to TestBeanOverrideProcessor: expected @TestBean" +
|
||||
" on field %s.%s", field.getDeclaringClass().getName(), field.getName());
|
||||
}
|
||||
|
||||
|
||||
static class MethodConventionConf {
|
||||
|
||||
@TestBean
|
||||
@TestBean(name = "someField")
|
||||
public ExampleService field;
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -29,6 +29,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
|||
public @interface ExampleBeanOverrideAnnotation {
|
||||
|
||||
String DEFAULT_VALUE = "TEST OVERRIDE";
|
||||
// Any metadata using this as the prefix for the bean name will be considered equal
|
||||
String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
String value() default DEFAULT_VALUE;
|
||||
|
||||
|
|
|
@ -26,24 +26,15 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
|
|||
// Intentionally NOT public
|
||||
class ExampleBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
static final String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{DUPLICATE_TRIGGER}";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) {
|
||||
throw new IllegalStateException("unexpected annotation");
|
||||
}
|
||||
if (annotation.value().equals(DUPLICATE_TRIGGER)) {
|
||||
return CONSTANT;
|
||||
if (annotation.value().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER)) {
|
||||
return new TestOverrideMetadata(annotation.value());
|
||||
}
|
||||
return new TestOverrideMetadata(field, annotation, typeToOverride);
|
||||
return new TestOverrideMetadata(field, annotation, ResolvableType.forField(field, testClass));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ class TestOverrideMetadata extends OverrideMetadata {
|
|||
@Nullable
|
||||
private final String beanName;
|
||||
|
||||
private final String methodName;
|
||||
|
||||
@Nullable
|
||||
private static Method findMethod(AnnotatedElement element, String methodName) {
|
||||
if (DEFAULT_VALUE.equals(methodName)) {
|
||||
|
@ -77,30 +79,31 @@ class TestOverrideMetadata extends OverrideMetadata {
|
|||
}
|
||||
|
||||
public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ?
|
||||
super(field, typeToOverride, overrideAnnotation.createIfMissing() ?
|
||||
BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
this.method = findMethod(field, overrideAnnotation.value());
|
||||
this.methodName = overrideAnnotation.value();
|
||||
this.beanName = overrideAnnotation.beanName();
|
||||
}
|
||||
|
||||
//Used to trigger duplicate detection in parser test
|
||||
TestOverrideMetadata() {
|
||||
super(null, null, null, null);
|
||||
TestOverrideMetadata(String duplicateTrigger) {
|
||||
super(null, null, null);
|
||||
this.method = null;
|
||||
this.beanName = null;
|
||||
this.methodName = duplicateTrigger;
|
||||
this.beanName = duplicateTrigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getExpectedBeanName() {
|
||||
protected String getBeanName() {
|
||||
if (StringUtils.hasText(this.beanName)) {
|
||||
return this.beanName;
|
||||
}
|
||||
return super.getExpectedBeanName();
|
||||
return super.getBeanName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return "test";
|
||||
String getAnnotationMethodName() {
|
||||
return this.methodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -117,4 +120,29 @@ class TestOverrideMetadata extends OverrideMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this.method == null) {
|
||||
return obj instanceof TestOverrideMetadata tem &&
|
||||
tem.beanName != null &&
|
||||
tem.beanName.startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (this.method == null) {
|
||||
return ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER.hashCode();
|
||||
}
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (this.method == null) {
|
||||
return "{" + this.beanName + "}";
|
||||
}
|
||||
return this.methodName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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 org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringJUnitConfig
|
||||
public class MockitoBeanIntegrationTests {
|
||||
|
||||
@MockitoBean
|
||||
ExampleService field;
|
||||
|
||||
@MockitoBean
|
||||
ExampleService nestedField;
|
||||
|
||||
@MockitoBean(name = "field")
|
||||
ExampleService renamed1;
|
||||
|
||||
@MockitoBean(name = "nestedField")
|
||||
ExampleService renamed2;
|
||||
|
||||
@MockitoBean(name = "nonExistingBean")
|
||||
ExampleService nonExisting1;
|
||||
|
||||
@MockitoBean(name = "nestedNonExistingBean")
|
||||
ExampleService nonExisting2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.field);
|
||||
|
||||
assertThat(this.field.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.renamed1);
|
||||
|
||||
assertThat(this.renamed1.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nonExistingBean"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.nonExisting1);
|
||||
|
||||
assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoBean on enclosing class")
|
||||
class MockitoBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.nestedField);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.renamed2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedNonExistingBean"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.nonExisting2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Bean("field")
|
||||
ExampleService bean1() {
|
||||
return new RealExampleService("Hello Field");
|
||||
}
|
||||
@Bean("nestedField")
|
||||
ExampleService bean2() {
|
||||
return new RealExampleService("Hello Nested Field");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
public class MockitoBeanOverrideProcessorTests {
|
||||
|
||||
@Test
|
||||
void mockAnnotationCreatesMockMetadata() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
|
||||
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoBeanMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void spyAnnotationCreatesSpyMetadata() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
|
||||
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherAnnotationThrows() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
Annotation annotation = field.getAnnotation(Nullable.class);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(annotation, clazz, field))
|
||||
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected " +
|
||||
"@MockitoBean/@MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
|
||||
field.getName());
|
||||
}
|
||||
|
||||
static class MockitoConf {
|
||||
@Nullable
|
||||
@MockitoBean
|
||||
@MockitoSpyBean
|
||||
public ExampleService a;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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 org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
@SpringJUnitConfig(MockitoBeanIntegrationTests.Config.class)
|
||||
public class MockitoSpyBeanIntegrationTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService field;
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService nestedField;
|
||||
|
||||
@MockitoSpyBean(name = "field")
|
||||
ExampleService renamed1;
|
||||
|
||||
@MockitoSpyBean(name = "nestedField")
|
||||
ExampleService renamed2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(this.field);
|
||||
|
||||
assertThat(this.field.greeting()).as("spied greeting")
|
||||
.isEqualTo("Hello Field");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(this.renamed1);
|
||||
|
||||
assertThat(this.field.greeting()).as("spied greeting")
|
||||
.isEqualTo("Hello Field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failWhenBeanNotPresentFieldName() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failure1.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresent' by wrapping," +
|
||||
" no existing bean instance by this name of type %s",
|
||||
ExampleService.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failWhenBeanNotPresentExplicitName() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failure2.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresentAtAll' by wrapping," +
|
||||
" no existing bean instance by this name of type %s",
|
||||
ExampleService.class.getName()));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoSpyBean on enclosing class")
|
||||
class MockitoBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(MockitoSpyBeanIntegrationTests.this.nestedField);
|
||||
|
||||
assertThat(MockitoSpyBeanIntegrationTests.this.nestedField.greeting())
|
||||
.as("spied greeting")
|
||||
.isEqualTo("Hello Nested Field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(MockitoSpyBeanIntegrationTests.this.renamed2);
|
||||
|
||||
|
||||
assertThat(MockitoSpyBeanIntegrationTests.this.renamed2.greeting())
|
||||
.as("spied greeting")
|
||||
.isEqualTo("Hello Nested Field");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failure1 {
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService notPresent;
|
||||
|
||||
@Test
|
||||
void ignored() { }
|
||||
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failure2 {
|
||||
|
||||
@MockitoSpyBean(name = "notPresentAtAll")
|
||||
ExampleService field;
|
||||
|
||||
@Test
|
||||
void ignored() { }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue