Add support for bean overriding in tests

This commit introduces two sets of annotations (`@TestBean` on one side
and `MockitoBean`/`MockitoSpyBean` on the other side), as well as an
extension mecanism based on the `@BeanOverride` meta-annotation.

Extension implementors are expected to only provide an annotation,
a BeanOverrideProcessor implementation and an OverrideMetadata subclass.

Closes gh-29917.
This commit is contained in:
Simon Baslé 2023-12-14 11:43:37 +01:00
parent 90867e7e62
commit e1bbdf0913
41 changed files with 3516 additions and 3 deletions

View File

@ -183,6 +183,7 @@
***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[]
***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[]
***** xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[]
***** xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc[]
**** xref:testing/annotations/integration-junit4.adoc[]
**** xref:testing/annotations/integration-junit-jupiter.adoc[]
**** xref:testing/annotations/integration-meta.adoc[]

View File

@ -28,4 +28,6 @@ Spring's testing annotations include the following:
* xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`]
* xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`]
* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-testbean[`@TestBean`]
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-mockitobean[`@MockitoBean` and `@MockitoSpyBean`]

View File

@ -0,0 +1,130 @@
[[spring-testing-annotation-beanoverriding]]
= Bean Overriding in Tests
Bean Overriding in Tests refers to the ability to override specific beans in the Context
for a test class, by annotating one or more fields in said test class.
NOTE: This is intended as a less risky alternative to the practice of registering a bean via
`@Bean` with the `DefaultListableBeanFactory` `setAllowBeanDefinitionOverriding` set to
`true`.
The Spring Testing Framework provides two sets of annotations presented below. One relies
purely on Spring, while the second set relies on the Mockito third party library.
[[spring-testing-annotation-beanoverriding-testbean]]
== `@TestBean`
`@TestBean` is used on a test class field to override a specific bean with an instance
provided by a conventionally named static method.
By default, the bean name and the associated static method name are derived from the
annotated field's name, but the annotation allows for specific values to be provided.
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
The following example shows how to fully configure the `@TestBean` annotation, with
explicit values equivalent to the default:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
class OverrideBeanTests {
@TestBean(name = "service", methodName = "serviceTestOverride") // <1>
private CustomService service;
// test case body...
private static CustomService serviceTestOverride() { // <2>
return new MyFakeCustomService();
}
}
----
<1> Mark a field for bean overriding in this test class
<2> The result of this static method will be used as the instance and injected into the field
======
[[spring-testing-annotation-beanoverriding-mockitobean]]
== `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on a test class field to override a bean
with a mocking and spying instance, respectively. In the later case, the original bean
definition is not replaced but instead an early instance is captured and wrapped by the
spy.
By default, the name of the bean to override is derived from the annotated field's name,
but both annotations allows for a specific `name` to be provided. Each annotation also
defines Mockito-specific attributes to fine-tune the mocking details.
The `@MockitoBean` annotation uses the `CREATE_OR_REPLACE_DEFINITION`
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
The `@MockitoSpyBean` annotation uses the `WRAP_EARLY_BEAN`
xref:#spring-testing-annotation-beanoverriding-extending[strategy] and the original instance
is wrapped in a Mockito spy.
The following example shows how to configure the bean name for both `@MockitoBean` and
`@MockitoSpyBean` annotations:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
class OverrideBeanTests {
@MockitoBean(name = "service1") // <1>
private CustomService mockService;
@MockitoSpyBean(name = "service2") // <2>
private CustomService spyService; // <3>
// test case body...
}
----
<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class
<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class
<3> Both fields will be injected with the Mockito values (the mock and the spy respectively)
======
[[spring-testing-annotation-beanoverriding-extending]]
== Extending bean override with a custom annotation
The three annotations introduced above build upon the `@BeanOverride` meta-annotation
and associated infrastructure, which allows to define custom bean overriding variants.
In order to provide an extension, three classes are needed:
- a concrete `BeanOverrideProcessor` `<P>`
- a concrete `OverrideMetadata` 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.
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
`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.
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.

View File

@ -42,6 +42,7 @@ dependencies {
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
optional("org.junit.jupiter:junit-jupiter-api")
optional("org.junit.platform:junit-platform-launcher") // for AOT processing
optional("org.mockito:mockito-core")
optional("org.seleniumhq.selenium:htmlunit-driver") {
exclude group: "commons-logging", module: "commons-logging"
exclude group: "net.bytebuddy", module: "byte-buddy"
@ -79,6 +80,7 @@ dependencies {
testImplementation("org.hibernate:hibernate-validator")
testImplementation("org.hsqldb:hsqldb")
testImplementation("org.junit.platform:junit-platform-testkit")
testImplementation("org.mockito:mockito-core")
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
testRuntimeOnly("com.sun.xml.bind:jaxb-impl")
testRuntimeOnly("org.glassfish:jakarta.el")

View File

@ -0,0 +1,46 @@
/*
* 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.bean.override;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mark an annotation as eligible for Bean Override parsing.
* This meta-annotation provides a {@link BeanOverrideProcessor} class which
* must be capable of handling the annotated annotation.
*
* <p>Target annotation must have a {@link RetentionPolicy} of {@code RUNTIME}
* and be applicable to {@link java.lang.reflect.Field Fields} only.
* @see BeanOverrideBeanPostProcessor
*
* @author Simon Baslé
* @since 6.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface BeanOverride {
/**
* A {@link BeanOverrideProcessor} implementation class by which the target
* annotation should be processed. Implementations must have a no-argument
* constructor.
*/
Class<? extends BeanOverrideProcessor> value();
}

View File

@ -0,0 +1,370 @@
/*
* 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.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}. A set of
* {@link OverrideMetadata} must be passed to the processor.
* A {@link BeanOverrideParser} can typically be used to parse these from test
* classes that use any annotation meta-annotated with {@link BeanOverride} to
* mark override sites.
*
* <p>This processor supports two {@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 Set<OverrideMetadata> overrideMetadata;
private final Map<String, OverrideMetadata> earlyOverrideMetadata = new HashMap<>();
private ConfigurableListableBeanFactory beanFactory;
private final Map<OverrideMetadata, String> beanNameRegistry = new HashMap<>();
private final Map<Field, String> fieldRegistry = new HashMap<>();
/**
* Create a new {@link 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,
"Beans 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 postProcess");
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
Set<OverrideMetadata> overrideMetadata = getOverrideMetadata();
for (OverrideMetadata metadata : overrideMetadata) {
registerBeanOverride(registry, metadata);
}
}
/**
* Copy the details of a {@link BeanDefinition} to the definition created by
* this processor for a given {@link OverrideMetadata}. Defaults to copying
* the {@link BeanDefinition#isPrimary()} attribute and 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);
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.
* If so, put the override metadata in the early tracking map.
* 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 overrides 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 {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
* and {@link 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);
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) {
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(this::isScopedTarget);
return beans;
}
private boolean isScopedTarget(String beanName) {
try {
return ScopedProxyUtils.isScopedTarget(beanName);
}
catch (Throwable ex) {
return false;
}
}
private void postProcessField(Object bean, Field field) {
String beanName = this.fieldRegistry.get(field);
if (StringUtils.hasText(beanName)) {
inject(field, bean, beanName);
}
}
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field));
return pvs;
}
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 {
field.setAccessible(true);
Object existingValue = ReflectionUtils.getField(field, target);
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 the processor with a {@link BeanDefinitionRegistry}.
* 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,
constructorArguments -> constructorArguments.addIndexedArgumentValue(0,
new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME)));
//main processor
BeanDefinition definition = getOrAddInfrastructureBeanDefinition(registry, BeanOverrideBeanPostProcessor.class,
INFRASTRUCTURE_BEAN_NAME, constructorArguments -> constructorArguments
.addIndexedArgumentValue(0, new LinkedHashSet<OverrideMetadata>()));
ConstructorArgumentValues.ValueHolder constructorArg = definition.getConstructorArgumentValues()
.getIndexedArgumentValue(0, Set.class);
@SuppressWarnings("unchecked")
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();
}
}
}

View File

@ -0,0 +1,100 @@
/*
* 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.bean.override;
import java.util.List;
import java.util.Set;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* A {@link ContextCustomizerFactory} to add support for Bean Overriding.
*
* @author Simon Baslé
* @since 6.2
*/
public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
BeanOverrideParser parser = new BeanOverrideParser();
parseMetadata(testClass, parser);
if (parser.getOverrideMetadata().isEmpty()) {
return null;
}
return new BeanOverrideContextCustomizer(parser.getOverrideMetadata());
}
private void parseMetadata(Class<?> testClass, BeanOverrideParser parser) {
parser.parse(testClass);
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
parseMetadata(testClass.getEnclosingClass(), parser);
}
}
/**
* A {@link ContextCustomizer} for Bean Overriding in tests.
*/
@Reflective
static final class BeanOverrideContextCustomizer implements ContextCustomizer {
private final Set<OverrideMetadata> metadata;
/**
* 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;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
if (context instanceof BeanDefinitionRegistry registry) {
BeanOverrideBeanPostProcessor.register(registry, this.metadata);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj;
return this.metadata.equals(other.metadata);
}
@Override
public int hashCode() {
return this.metadata.hashCode();
}
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* A parser that discovers annotations meta-annotated with {@link BeanOverride}
* on fields of a given class and creates {@link OverrideMetadata} accordingly.
*
* @author Simon Baslé
*/
class BeanOverrideParser {
private final Set<OverrideMetadata> parsedMetadata;
BeanOverrideParser() {
this.parsedMetadata = new LinkedHashSet<>();
}
/**
* Getter for 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}, then instantiate their corresponding
* {@link BeanOverrideProcessor} and use it to create an {@link OverrideMetadata}
* instance for each field. Each call to {@code parse} adds the parsed
* metadata to the parser's override metadata {{@link #getOverrideMetadata()}
* set}
* @param testClass the class which fields to inspect
*/
void parse(Class<?> testClass) {
ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass));
}
/**
* Check if any field of the provided {@code testClass} is meta-annotated
* with {@link 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}, so this method leaves the current state of
* {@link #getOverrideMetadata()} unchanged.
* @param testClass the class which fields to inspect
* @return true if there is a bean override annotation present, false otherwise
* @see #parse(Class)
*/
boolean hasBeanOverride(Class<?> testClass) {
AtomicBoolean hasBeanOverride = new AtomicBoolean();
ReflectionUtils.doWithFields(testClass, field -> {
if (hasBeanOverride.get()) {
return;
}
final long count = MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT)
.stream(BeanOverride.class)
.count();
hasBeanOverride.compareAndSet(false, count > 0L);
});
return hasBeanOverride.get();
}
private void parseField(Field field, Class<?> source) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT)
.stream(BeanOverride.class)
.map(bo -> {
var a = bo.getMetaSource();
Assert.notNull(a, "BeanOverride annotation must be meta-present");
return new AnnotationPair(a.synthesize(), bo);
})
.forEach(pair -> {
var metaAnnotation = pair.metaAnnotation().synthesize();
final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value());
if (processor == null) {
return;
}
ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.annotation(), source);
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
"Multiple bean override annotations found on annotated field <" + field + ">");
OverrideMetadata metadata = processor.createMetadata(field, pair.annotation(), typeToOverride);
boolean isNewDefinition = this.parsedMetadata.add(metadata);
Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() +
" overrideMetadata " + metadata);
});
}
@Nullable
private BeanOverrideProcessor getProcessorInstance(Class<? extends BeanOverrideProcessor> processorClass) {
final Constructor<? extends BeanOverrideProcessor> constructor = ClassUtils.getConstructorIfAvailable(processorClass);
if (constructor != null) {
ReflectionUtils.makeAccessible(constructor);
try {
return constructor.newInstance();
}
catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
throw new BeanDefinitionValidationException("Could not get an instance of BeanOverrideProcessor", ex);
}
}
return null;
}
private record AnnotationPair(Annotation annotation, MergedAnnotation<BeanOverride> metaAnnotation) {}
}

View File

@ -0,0 +1,70 @@
/*
* 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.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;
import org.springframework.core.annotation.MergedAnnotation;
/**
* An interface for Bean Overriding concrete processing.
* Processors are generally linked to one or more specific concrete annotations
* (meta-annotated with {@link 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>Implementations are required to have a no-argument constructor and be
* stateless.
*
* @author Simon Baslé
* @since 6.2
*/
@FunctionalInterface
public interface BeanOverrideProcessor {
/**
* Determine a {@link ResolvableType} for which an {@link OverrideMetadata}
* instance will be created, e.g. by using the annotation to determine the
* type.
* <p>Defaults to the field corresponding {@link ResolvableType},
* additionally tracking the source class if the field 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} for a given annotated field and target
* {@link #getOrDeduceType(Field, Annotation, Class) type}.
* Specific implementations of metadata can have state to be used during
* override {@link OverrideMetadata#createOverride(String, BeanDefinition,
* Object) instance creation} (e.g. from further parsing the annotation or
* the annotated field).
* @param field the annotated field
* @param overrideAnnotation the field annotation
* @param typeToOverride the target type
* @return a new {@link OverrideMetadata}
* @see #getOrDeduceType(Field, Annotation, Class)
* @see MergedAnnotation#synthesize()
*/
OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride);
}

View File

@ -0,0 +1,45 @@
/*
* 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.bean.override;
/**
* Strategies for override instantiation, implemented in
* {@link BeanOverrideBeanPostProcessor}.
*
* @author Simon Baslé
* @since 6.2
*/
public enum BeanOverrideStrategy {
/**
* Replace a given bean's definition, immediately preparing a singleton
* instance. Enforces the original bean definition to exist.
*/
REPLACE_DEFINITION,
/**
* Replace a given bean's definition, immediately preparing a singleton
* instance. If the original bean definition does not exist, create the
* override definition instead of failing.
*/
REPLACE_OR_CREATE_DEFINITION,
/**
* Intercept and wrap the actual bean instance upon creation, during
* {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)
* early bean definition}.
*/
WRAP_EARLY_BEAN;
}

View File

@ -0,0 +1,107 @@
/*
* 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.bean.override;
import java.lang.reflect.Field;
import java.util.function.BiConsumer;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.util.ReflectionUtils;
/**
* A {@link TestExecutionListener} that enables Bean Override support in
* tests, injecting overridden beans in appropriate fields.
*
* <p>Some flavors of Bean Override might additionally require the use of
* additional listeners, which should be mentioned in the annotation(s) javadoc.
*
* @author Simon Baslé
* @since 6.2
*/
public class BeanOverrideTestExecutionListener extends AbstractTestExecutionListener {
/**
* Executes almost last ({@code LOWEST_PRECEDENCE - 50}).
*/
@Override
public int getOrder() {
return LOWEST_PRECEDENCE - 50;
}
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
injectFields(testContext);
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
reinjectFieldsIfConfigured(testContext);
}
/**
* Using a registered {@link BeanOverrideBeanPostProcessor}, 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()));
}
/**
* Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata
* associated with the current test class and ensure fields are nulled out
* then re-injected with the overridden bean instance. This method does
* nothing if the {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* attribute is not present in the {@code testContext}.
*/
protected void reinjectFieldsIfConfigured(final TestContext testContext) throws Exception {
if (Boolean.TRUE.equals(
testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) {
postProcessFields(testContext, (testMetadata, postProcessor) -> {
Field f = testMetadata.overrideMetadata.field();
ReflectionUtils.makeAccessible(f);
ReflectionUtils.setField(f, testMetadata.testInstance(), null);
postProcessor.inject(f, testMetadata.testInstance(), testMetadata.overrideMetadata());
});
}
}
private void postProcessFields(TestContext testContext, BiConsumer<TestContextOverrideMetadata,
BeanOverrideBeanPostProcessor> consumer) {
//avoid full parsing but validate that this particular class has some bean override field(s)
BeanOverrideParser parser = new BeanOverrideParser();
if (parser.hasBeanOverride(testContext.getTestClass())) {
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(testContext.getTestClass())) {
continue;
}
consumer.accept(new TestContextOverrideMetadata(testContext.getTestInstance(), metadata),
postProcessor);
}
}
}
private record TestContextOverrideMetadata(Object testInstance, OverrideMetadata overrideMetadata) {}
}

View File

@ -0,0 +1,153 @@
/*
* 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.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Objects;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
/**
* Metadata for Bean Overrides.
*
* @author Simon Baslé
* @since 6.2
*/
public abstract class OverrideMetadata {
private final Field field;
private final Annotation overrideAnnotation;
private final ResolvableType typeToOverride;
private final BeanOverrideStrategy strategy;
public OverrideMetadata(Field field, Annotation overrideAnnotation,
ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
this.field = field;
this.overrideAnnotation = overrideAnnotation;
this.typeToOverride = typeToOverride;
this.strategy = strategy;
}
/**
* Define a short human-readable description of the kind of override this
* OverrideMetadata is about. This is especially useful for
* {@link BeanOverrideProcessor} that produce several subtypes of metadata
* (e.g. "mock" vs "spy").
*/
public abstract String getBeanOverrideDescription();
/**
* Provide the expected bean name to override. Typically, this is either
* explicitly set in the concrete annotations or defined by the annotated
* field's name.
* @return the expected bean name, not null
*/
protected String getExpectedBeanName() {
return this.field.getName();
}
/**
* The field annotated with a {@link BeanOverride}-compatible annotation.
* @return the annotated field
*/
public Field field() {
return this.field;
}
/**
* The concrete override annotation, i.e. the one meta-annotated with
* {@link BeanOverride}.
*/
public Annotation overrideAnnotation() {
return this.overrideAnnotation;
}
/**
* The type to override, as a {@link ResolvableType}.
*/
public ResolvableType typeToOverride() {
return this.typeToOverride;
}
/**
* Define the broad {@link BeanOverrideStrategy} for this
* {@link OverrideMetadata}, as a hint on how and when the override instance
* should be created.
*/
public final BeanOverrideStrategy getBeanOverrideStrategy() {
return this.strategy;
}
/**
* Create an override instance from this {@link OverrideMetadata},
* optionally provided with an existing {@link BeanDefinition} and/or an
* original instance (i.e. a singleton or an early wrapped instance).
* @param beanName the name of the bean being overridden
* @param existingBeanDefinition an existing bean definition for that bean
* name, or {@code null} if not relevant
* @param existingBeanInstance an existing instance for that bean name,
* for wrapping purpose, or {@code null} if irrelevant
* @return the instance with which to override the bean
*/
protected abstract Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance);
/**
* Optionally track objects created by this {@link OverrideMetadata}
* (default is no tracking).
* @param override the bean override instance to track
* @param trackingBeanRegistry the registry in which trackers could
* optionally be registered
*/
protected void track(Object override, SingletonBeanRegistry trackingBeanRegistry) {
//NO-OP
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
var that = (OverrideMetadata) obj;
return Objects.equals(this.field, that.field) &&
Objects.equals(this.overrideAnnotation, that.overrideAnnotation) &&
Objects.equals(this.strategy, that.strategy) &&
Objects.equals(this.typeToOverride, that.typeToOverride);
}
@Override
public int hashCode() {
return Objects.hash(this.field, this.overrideAnnotation, this.strategy, this.typeToOverride);
}
@Override
public String toString() {
return "OverrideMetadata[" +
"category=" + this.getBeanOverrideDescription() + ", " +
"field=" + this.field + ", " +
"overrideAnnotation=" + this.overrideAnnotation + ", " +
"strategy=" + this.strategy + ", " +
"typeToOverride=" + this.typeToOverride + ']';
}
}

View File

@ -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.bean.override.convention;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.test.bean.override.BeanOverride;
/**
* Mark a field to represent a "method" bean override of the bean of the same
* name and inject the field with the overriding instance.
*
* <p>The instance is created from a static method in the declaring class which
* return type is compatible with the annotated field and which name follows the
* convention:
* <ul>
* <li>if the annotation's {@link #methodName()} is specified,
* look for that one.</li>
* <li>if not, look for exactly one method named with the
* {@link #CONVENTION_SUFFIX} suffix and either:</li>
* <ul>
* <li>starting with the annotated field name</li>
* <li>starting with the bean name</li>
* </ul>
* </ul>
*
* <p>The annotated field's name is interpreted to be the name of the original
* bean to override, unless the annotation's {@link #name()} is specified.
*
* @see TestBeanOverrideProcessor
* @author Simon Baslé
* @since 6.2
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@BeanOverride(TestBeanOverrideProcessor.class)
public @interface TestBean {
/**
* The method suffix expected as a convention in static methods which
* provides an override instance.
*/
String CONVENTION_SUFFIX = "TestOverride";
/**
* The name of a static method to look for in the Configuration, which will
* be used to instantiate the override bean and inject the annotated field.
* <p> Default is {@code ""} (the empty String), which is translated into
* the annotated field's name concatenated with the
* {@link #CONVENTION_SUFFIX}.
*/
String methodName() default "";
/**
* The name of the original bean to override, or {@code ""} (the empty
* String) to deduce the name from the annotated field.
*/
String name() default "";
}

View File

@ -0,0 +1,145 @@
/*
* 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.bean.override.convention;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.test.bean.override.BeanOverrideProcessor;
import org.springframework.test.bean.override.BeanOverrideStrategy;
import org.springframework.test.bean.override.OverrideMetadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Simple {@link BeanOverrideProcessor} primarily made to work with the
* {@link TestBean} annotation but can work with arbitrary override annotations
* provided the annotated class has a relevant method according to the
* convention documented in {@link TestBean}.
*
* @author Simon Baslé
* @since 6.2
*/
public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
/**
* Ensures the {@code enclosingClass} has a static, no-arguments method with
* the provided {@code expectedMethodReturnType} and exactly one of the
* {@code expectedMethodNames}.
*/
public static Method ensureMethod(Class<?> enclosingClass, Class<?> expectedMethodReturnType,
String... expectedMethodNames) {
Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required");
Set<String> expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames));
final List<Method> found = Arrays.stream(enclosingClass.getDeclaredMethods())
.filter(m -> Modifier.isStatic(m.getModifiers()))
.filter(m -> expectedNames.contains(m.getName()) && expectedMethodReturnType
.isAssignableFrom(m.getReturnType()))
.collect(Collectors.toList());
Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " +
"instead of exactly one, matching a name in " + expectedNames + " with return type " +
expectedMethodReturnType.getName() + " on class " + enclosingClass.getName());
return found.get(0);
}
@Override
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
final Class<?> enclosingClass = field.getDeclaringClass();
// 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()) {
overrideMethod = ensureMethod(enclosingClass, field.getType(), testBeanAnnotation.methodName());
}
if (!testBeanAnnotation.name().isBlank()) {
beanName = testBeanAnnotation.name();
}
return new MethodConventionOverrideMetadata(field, overrideMethod, beanName,
overrideAnnotation, typeToOverride);
}
// otherwise defer the resolution of the static method until OverrideMetadata#createOverride
return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation,
typeToOverride);
}
static final class MethodConventionOverrideMetadata 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) {
super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
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 "method convention";
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance) {
Method methodToInvoke = this.overrideMethod;
if (methodToInvoke == null) {
methodToInvoke = ensureMethod(field().getDeclaringClass(), field().getType(),
beanName + TestBean.CONVENTION_SUFFIX,
field().getName() + TestBean.CONVENTION_SUFFIX);
}
methodToInvoke.setAccessible(true);
Object override;
try {
override = methodToInvoke.invoke(null);
}
catch (IllegalAccessException | InvocationTargetException ex) {
throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() +
", a static method with no input parameters is expected", ex);
}
return override;
}
}
}

View File

@ -0,0 +1,11 @@
/**
* Bean override mechanism based on conventionally-named static methods
* in the test class. This allows defining a custom instance for the bean
* straight from the test class.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.bean.override.convention;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,118 @@
/*
* Copyright 2012-2019 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.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.test.bean.override.BeanOverrideStrategy;
import org.springframework.test.bean.override.OverrideMetadata;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for {@link MockDefinition} and {@link SpyDefinition}.
*
* @author Phillip Webb
*/
abstract class Definition extends OverrideMetadata {
static final int MULTIPLIER = 31;
protected final String name;
private final MockReset reset;
private final boolean proxyTargetAware;
Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
super(field, annotation, typeToOverride, strategy);
this.name = name;
this.reset = (reset != null) ? reset : MockReset.AFTER;
this.proxyTargetAware = proxyTargetAware;
}
@Override
protected String getExpectedBeanName() {
if (StringUtils.hasText(this.name)) {
return this.name;
}
return super.getExpectedBeanName();
}
@Override
protected void track(Object mock, SingletonBeanRegistry trackingBeanRegistry) {
MockitoBeans tracker = null;
try {
tracker = (MockitoBeans) trackingBeanRegistry.getSingleton(MockitoBeans.class.getName());
}
catch (NoSuchBeanDefinitionException ignored) {
}
if (tracker == null) {
tracker= new MockitoBeans();
trackingBeanRegistry.registerSingleton(MockitoBeans.class.getName(), tracker);
}
tracker.add(mock);
}
/**
* Return the mock reset mode.
* @return the reset mode
*/
MockReset getReset() {
return this.reset;
}
/**
* Return if AOP advised beans should be proxy target aware.
* @return if proxy target aware
*/
boolean isProxyTargetAware() {
return this.proxyTargetAware;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
Definition other = (Definition) obj;
boolean 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;
}
@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;
}
}

View File

@ -0,0 +1,170 @@
/*
* Copyright 2012-2023 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.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.Set;
import org.mockito.Answers;
import org.mockito.MockSettings;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.bean.override.BeanOverrideStrategy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.mockito.Mockito.mock;
/**
* A complete definition that can be used to create a Mockito mock.
*
* @author Phillip Webb
*/
class MockDefinition extends Definition {
private static final int MULTIPLIER = 31;
private final Set<Class<?>> extraInterfaces;
private final Answers answer;
private final boolean serializable;
MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) {
this(annotation.name(), annotation.reset(), field, annotation, typeToMock,
annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
}
MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock,
Class<?>[] extraInterfaces, @Nullable Answers answer, boolean serializable) {
super(name, reset, false, field, annotation, 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 "mock";
}
@Override
protected Object createOverride(String beanName, BeanDefinition existingBeanDefinition, Object existingBeanInstance) {
return createMock(beanName);
}
private Set<Class<?>> asClassSet(Class<?>[] classes) {
Set<Class<?>> classSet = new LinkedHashSet<>();
if (classes != null) {
classSet.addAll(Arrays.asList(classes));
}
return Collections.unmodifiableSet(classSet);
}
/**
* Return the extra interfaces.
* @return the extra interfaces or an empty set
*/
Set<Class<?>> getExtraInterfaces() {
return this.extraInterfaces;
}
/**
* Return the answers mode.
* @return the answers mode; never {@code null}
*/
Answers getAnswer() {
return this.answer;
}
/**
* Return if the mock is serializable.
* @return if the mock is serializable
*/
boolean isSerializable() {
return this.serializable;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
MockDefinition other = (MockDefinition) obj;
boolean result = super.equals(obj);
result = result && ObjectUtils.nullSafeEquals(this.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;
return result;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride());
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer);
result = MULTIPLIER * result + Boolean.hashCode(this.serializable);
return result;
}
@Override
public String toString() {
return new ToStringCreator(this).append("name", this.name)
.append("typeToMock", this.typeToOverride())
.append("extraInterfaces", this.extraInterfaces)
.append("answer", this.answer)
.append("serializable", this.serializable)
.append("reset", getReset())
.toString();
}
<T> T createMock() {
return createMock(this.name);
}
@SuppressWarnings("unchecked")
<T> T createMock(String name) {
MockSettings settings = MockReset.withSettings(getReset());
if (StringUtils.hasLength(name)) {
settings.name(name);
}
if (!this.extraInterfaces.isEmpty()) {
settings.extraInterfaces(ClassUtils.toClassArray(this.extraInterfaces));
}
settings.defaultAnswer(this.answer);
if (this.serializable) {
settings.serializable();
}
return (T) mock(this.typeToOverride().resolve(), settings);
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright 2012-2023 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.bean.override.mockito;
import java.util.List;
import org.mockito.MockSettings;
import org.mockito.MockingDetails;
import org.mockito.Mockito;
import org.mockito.listeners.InvocationListener;
import org.mockito.listeners.MethodInvocationReport;
import org.mockito.mock.MockCreationSettings;
import org.springframework.util.Assert;
/**
* Reset strategy used on a mock bean. Usually applied to a mock through the
* {@link MockitoBean @MockitoBean} annotation but can also be directly applied to any mock in
* the {@code ApplicationContext} using the static methods.
*
* @author Phillip Webb
* @since 1.4.0
* @see MockitoResetTestExecutionListener
*/
public enum MockReset {
/**
* Reset the mock before the test method runs.
*/
BEFORE,
/**
* Reset the mock after the test method runs.
*/
AFTER,
/**
* Don't reset the mock.
*/
NONE;
/**
* Create {@link MockSettings settings} to be used with mocks where reset should occur
* before each test method runs.
* @return mock settings
*/
public static MockSettings before() {
return withSettings(BEFORE);
}
/**
* Create {@link MockSettings settings} to be used with mocks where reset should occur
* after each test method runs.
* @return mock settings
*/
public static MockSettings after() {
return withSettings(AFTER);
}
/**
* Create {@link MockSettings settings} to be used with mocks where a specific reset
* should occur.
* @param reset the reset type
* @return mock settings
*/
public static MockSettings withSettings(MockReset reset) {
return apply(reset, Mockito.withSettings());
}
/**
* Apply {@link MockReset} to existing {@link MockSettings settings}.
* @param reset the reset type
* @param settings the settings
* @return the configured settings
*/
public static MockSettings apply(MockReset reset, MockSettings settings) {
Assert.notNull(settings, "Settings must not be null");
if (reset != null && reset != NONE) {
settings.invocationListeners(new ResetInvocationListener(reset));
}
return settings;
}
/**
* Get the {@link MockReset} associated with the given mock.
* @param mock the source mock
* @return the reset type (never {@code null})
*/
static MockReset get(Object mock) {
MockReset reset = MockReset.NONE;
MockingDetails mockingDetails = Mockito.mockingDetails(mock);
if (mockingDetails.isMock()) {
MockCreationSettings<?> settings = mockingDetails.getMockCreationSettings();
List<InvocationListener> listeners = settings.getInvocationListeners();
for (Object listener : listeners) {
if (listener instanceof ResetInvocationListener resetInvocationListener) {
reset = resetInvocationListener.getReset();
}
}
}
return reset;
}
/**
* Dummy {@link InvocationListener} used to hold the {@link MockReset} value.
*/
private static class ResetInvocationListener implements InvocationListener {
private final MockReset reset;
ResetInvocationListener(MockReset reset) {
this.reset = reset;
}
MockReset getReset() {
return this.reset;
}
@Override
public void reportInvocation(MethodInvocationReport methodInvocationReport) {
}
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.mockito.Answers;
import org.mockito.MockSettings;
import org.springframework.test.bean.override.BeanOverride;
/**
* Mark a field to trigger a bean override using a Mockito mock. If no explicit
* {@link #name()} is specified, the annotated field's name is interpreted to
* be the target of the override. In either case, if no existing bean is defined
* a new one will be added to the context. In order to ensure mocks are set up
* and reset correctly, the test class must itself be annotated with
* {@link MockitoBeanOverrideTestListeners}.
*
* <p>Dependencies that are known to the application context but are not beans
* (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly}) will not be found and a mocked bean will be added to
* the context alongside the existing dependency.
*
* @author Simon Baslé
* @since 6.2
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoBean {
/**
* The name of the bean to register or replace. If not specified, it will be
* the name of the annotated field.
* @return the name of the bean
*/
String name() default "";
/**
* Any extra interfaces that should also be declared on the mock. See
* {@link MockSettings#extraInterfaces(Class...)} for details.
* @return any extra interfaces
*/
Class<?>[] extraInterfaces() default {};
/**
* The {@link Answers} type to use on the mock.
* @return the answer type
*/
Answers answers() default Answers.RETURNS_DEFAULTS;
/**
* If the generated mock is serializable. See {@link MockSettings#serializable()} for
* details.
* @return if the mock is serializable
*/
boolean serializable() default false;
/**
* The reset mode to apply to the mock bean. The default is {@link MockReset#AFTER}
* meaning that mocks are automatically reset after each test method is invoked.
* @return the reset mode
*/
MockReset reset() default MockReset.AFTER;
}

View File

@ -0,0 +1,38 @@
/*
* 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.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.springframework.core.ResolvableType;
import org.springframework.test.bean.override.BeanOverrideProcessor;
import org.springframework.test.bean.override.OverrideMetadata;
public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) {
if (overrideAnnotation instanceof MockitoBean mockBean) {
return new MockDefinition(mockBean, field, typeToMock);
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
return new SpyDefinition(spyBean, field, typeToMock);
}
throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + overrideAnnotation.getClass().getName());
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2019 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.bean.override.mockito;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Beans created using Mockito.
*
* @author Andy Wilkinson
*/
class MockitoBeans implements Iterable<Object> {
private final List<Object> beans = new ArrayList<>();
void add(Object bean) {
this.beans.add(bean);
}
@Override
public Iterator<Object> iterator() {
return this.beans.iterator();
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2012-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.bean.override.mockito;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.NativeDetector;
import org.springframework.core.Ordered;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
/**
* {@link TestExecutionListener} to reset any mock beans that have been marked with a
* {@link MockReset}. Typically used alongside {@link MockitoTestExecutionListener}.
*
* @author Phillip Webb
* @since 6.2
* @see MockitoTestExecutionListener
*/
public class MockitoResetTestExecutionListener extends AbstractTestExecutionListener {
/**
* Executes before {@link org.springframework.test.bean.override.BeanOverrideTestExecutionListener}.
*/
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) {
resetMocks(testContext.getApplicationContext(), MockReset.BEFORE);
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) {
resetMocks(testContext.getApplicationContext(), MockReset.AFTER);
}
}
private void resetMocks(ApplicationContext applicationContext, MockReset reset) {
if (applicationContext instanceof ConfigurableApplicationContext configurableContext) {
resetMocks(configurableContext, reset);
}
}
private void resetMocks(ConfigurableApplicationContext applicationContext, MockReset reset) {
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
String[] names = beanFactory.getBeanDefinitionNames();
Set<String> instantiatedSingletons = new HashSet<>(Arrays.asList(beanFactory.getSingletonNames()));
for (String name : names) {
BeanDefinition definition = beanFactory.getBeanDefinition(name);
if (definition.isSingleton() && instantiatedSingletons.contains(name)) {
Object bean = getBean(beanFactory, name);
if (bean != null && reset.equals(MockReset.get(bean))) {
Mockito.reset(bean);
}
}
}
try {
MockitoBeans mockedBeans = beanFactory.getBean(MockitoBeans.class);
for (Object mockedBean : mockedBeans) {
if (reset.equals(MockReset.get(mockedBean))) {
Mockito.reset(mockedBean);
}
}
}
catch (NoSuchBeanDefinitionException ex) {
// Continue
}
if (applicationContext.getParent() != null) {
resetMocks(applicationContext.getParent(), reset);
}
}
private Object getBean(ConfigurableListableBeanFactory beanFactory, String name) {
try {
if (isStandardBeanOrSingletonFactoryBean(beanFactory, name)) {
return beanFactory.getBean(name);
}
}
catch (Exception ex) {
// Continue
}
return beanFactory.getSingleton(name);
}
private boolean isStandardBeanOrSingletonFactoryBean(ConfigurableListableBeanFactory beanFactory, String name) {
String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + name;
if (beanFactory.containsBean(factoryBeanName)) {
FactoryBean<?> factoryBean = (FactoryBean<?>) beanFactory.getBean(factoryBeanName);
return factoryBean.isSingleton();
}
return true;
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.mockito.Mockito;
import org.springframework.test.bean.override.BeanOverride;
/**
* Mark a field to trigger the override of the bean of the same name with a
* Mockito spy, which will wrap the original instance.
* In order to ensure mocks are set up and reset correctly, the test class must
* itself be annotated with {@link MockitoBeanOverrideTestListeners}.
*
* @author Simon Baslé
* @since 6.2
*/
/**
* Mark a field to trigger a bean override using a Mockito spy, which will wrap
* the original instance. If no explicit {@link #name()} is specified, the
* annotated field's name is interpreted to be the target of the override.
* In either case, it is required that the target bean is previously registered
* in the context. In order to ensure spies are set up and reset correctly,
* the test class must itself be annotated with {@link MockitoBeanOverrideTestListeners}.
*
* <p>Dependencies that are known to the application context but are not beans
* (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly}) will not be found.
*
* @author Simon Baslé
* @since 6.2
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoSpyBean {
/**
* The name of the bean to spy. If not specified, it will be the name of the
* annotated field.
* @return the name of the spied bean
*/
String name() default "";
/**
* The reset mode to apply to the spied bean. The default is {@link MockReset#AFTER}
* meaning that spies are automatically reset after each test method is invoked.
* @return the reset mode
*/
MockReset reset() default MockReset.AFTER;
/**
* Indicates that Mockito methods such as {@link Mockito#verify(Object) verify(mock)}
* should use the {@code target} of AOP advised beans, rather than the proxy itself.
* If set to {@code false} you may need to use the result of
* {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object)
* AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods.
* @return {@code true} if the target of AOP advised beans is used or {@code false} if
* the proxy is used directly
*/
boolean proxyTargetAware() default true;
}

View File

@ -0,0 +1,139 @@
/*
* Copyright 2012-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.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.LinkedHashSet;
import java.util.Set;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
/**
* {@link TestExecutionListener} to enable {@link MockitoBean @MockitoBean} and
* {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers
* {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations used,
* primarily to allow {@link Captor @Captor} annotations.
* <p>
* The automatic reset support of {@code @MockBean} and {@code @SpyBean} is
* handled by sibling {@link MockitoResetTestExecutionListener}.
*
* @author Simon Baslé
* @author Phillip Webb
* @author Andy Wilkinson
* @author Moritz Halbritter
* @since 1.4.2
* @see MockitoResetTestExecutionListener
*/
public class MockitoTestExecutionListener extends AbstractTestExecutionListener {
static final boolean mockitoPresent = ClassUtils.isPresent("org.mockito.MockSettings",
MockitoTestExecutionListener.class.getClassLoader());
private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks";
/**
* Executes before {@link DependencyInjectionTestExecutionListener}.
*/
@Override
public final int getOrder() {
return 1950;
}
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
if (mockitoPresent) {
closeMocks(testContext);
initMocks(testContext);
}
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
if (mockitoPresent && Boolean.TRUE.equals(
testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) {
closeMocks(testContext);
initMocks(testContext);
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
if (mockitoPresent) {
closeMocks(testContext);
}
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
if (mockitoPresent) {
closeMocks(testContext);
}
}
private void initMocks(TestContext testContext) {
if (hasMockitoAnnotations(testContext)) {
Object testInstance = testContext.getTestInstance();
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testInstance));
}
}
private void closeMocks(TestContext testContext) throws Exception {
Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME);
if (mocks instanceof AutoCloseable closeable) {
closeable.close();
}
}
private boolean hasMockitoAnnotations(TestContext testContext) {
MockitoAnnotationCollection collector = new MockitoAnnotationCollection();
ReflectionUtils.doWithFields(testContext.getTestClass(), collector);
return collector.hasAnnotations();
}
/**
* {@link FieldCallback} to collect Mockito annotations.
*/
private static final class MockitoAnnotationCollection implements FieldCallback {
private final Set<Annotation> annotations = new LinkedHashSet<>();
@Override
public void doWith(Field field) throws IllegalArgumentException {
for (Annotation annotation : field.getDeclaredAnnotations()) {
if (annotation.annotationType().getName().startsWith("org.mockito")) {
this.annotations.add(annotation);
}
}
}
boolean hasAnnotations() {
return !this.annotations.isEmpty();
}
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2012-2023 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.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;
import org.mockito.Mockito;
import org.mockito.listeners.VerificationStartedEvent;
import org.mockito.listeners.VerificationStartedListener;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.bean.override.BeanOverrideStrategy;
import org.springframework.test.util.AopTestUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.mockito.Mockito.mock;
/**
* A complete definition that can be used to create a Mockito spy.
*
* @author Phillip Webb
*/
class SpyDefinition extends Definition {
SpyDefinition(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(), field,
spyAnnotation, typeToSpy);
}
SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation,
ResolvableType typeToSpy) {
super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN);
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}
@Override
public String getBeanOverrideDescription() {
return "spy";
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
return createSpy(beanName, Objects.requireNonNull(existingBeanInstance,
"MockitoSpyBean requires an existing bean instance for bean " + beanName));
}
@Override
public boolean equals(@Nullable Object obj) {
//for SpyBean we want the class to be exactly the same
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
SpyDefinition other = (SpyDefinition) obj;
boolean result = super.equals(obj);
result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride());
return result;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride());
return result;
}
@Override
public String toString() {
return new ToStringCreator(this).append("name", this.name)
.append("typeToSpy", typeToOverride())
.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");
Assert.isInstanceOf(Objects.requireNonNull(this.typeToOverride().resolve()), instance);
if (Mockito.mockingDetails(instance).isSpy()) {
return (T) instance;
}
MockSettings settings = MockReset.withSettings(getReset());
if (StringUtils.hasLength(name)) {
settings.name(name);
}
if (isProxyTargetAware()) {
settings.verificationStartedListeners(new SpringAopBypassingVerificationStartedListener());
}
Class<?> toSpy;
if (Proxy.isProxyClass(instance.getClass())) {
settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance));
toSpy = this.typeToOverride().toClass();
}
else {
settings.defaultAnswer(Mockito.CALLS_REAL_METHODS);
settings.spiedInstance(instance);
toSpy = instance.getClass();
}
return (T) mock(toSpy, settings);
}
/**
* A {@link VerificationStartedListener} that bypasses any proxy created by Spring AOP
* when the verification of a spy starts.
*/
private static final class SpringAopBypassingVerificationStartedListener implements VerificationStartedListener {
@Override
public void onVerificationStarted(VerificationStartedEvent event) {
event.setMock(AopTestUtils.getUltimateTargetObject(event.getMock()));
}
}
}

View File

@ -0,0 +1,9 @@
/**
* Support case-by-case Bean overriding in Spring tests.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.bean.override.mockito;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,9 @@
/**
* Support case-by-case Bean overriding in Spring tests.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.bean.override;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -1,6 +1,9 @@
# Default TestExecutionListeners for the Spring TestContext Framework
#
org.springframework.test.context.TestExecutionListener = \
org.springframework.test.bean.override.BeanOverrideTestExecutionListener,\
org.springframework.test.bean.override.mockito.MockitoTestExecutionListener,\
org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener,\
org.springframework.test.context.web.ServletTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\
@ -14,5 +17,6 @@ org.springframework.test.context.TestExecutionListener = \
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
#
org.springframework.test.context.ContextCustomizerFactory = \
org.springframework.test.bean.override.BeanOverrideContextCustomizerFactory,\
org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory,\
org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory

View File

@ -0,0 +1,328 @@
/*
* 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.bean.override;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanWrapper;
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.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.SimpleThreadScope;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation;
import org.springframework.test.bean.override.example.ExampleService;
import org.springframework.test.bean.override.example.FailingExampleService;
import org.springframework.test.bean.override.example.RealExampleService;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Test for {@link BeanOverrideBeanPostProcessor}.
*
* @author Simon Baslé
*/
class BeanOverrideBeanPostProcessorTests {
BeanOverrideParser parser;
@BeforeEach
void initParser() {
this.parser = new BeanOverrideParser();
}
@Test
void canReplaceExistingBeanDefinitions() {
this.parser.parse(ReplaceBeans.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(ReplaceBeans.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
context.refresh();
assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE);
assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE);
}
@Test
void cannotReplaceIfNoBeanMatching() {
this.parser.parse(ReplaceBeans.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
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'");
}
@Test
void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() {
this.parser.parse(CreateIfOriginalIsMissingBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(CreateIfOriginalIsMissingBean.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
context.refresh();
assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE);
assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE);
}
@Test
void canCreateIfOriginalMissingWithCreateReplaceStrategy() {
this.parser.parse(CreateIfOriginalIsMissingBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(CreateIfOriginalIsMissingBean.class);
//note we don't register original beans here
context.refresh();
assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE);
assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE);
}
@Test
void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() {
this.parser.parse(OverriddenFactoryBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class);
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
context.register(OverriddenFactoryBean.class);
context.refresh();
assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE);
}
@Test
void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() {
this.parser.parse(OverriddenFactoryBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
ResolvableType objectType = ResolvableType.forClass(SomeInterface.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType);
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
context.register(OverriddenFactoryBean.class);
context.refresh();
assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE);
}
@Test
void postProcessorShouldNotTriggerEarlyInitialization() {
this.parser.parse(EagerInitBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(FactoryBeanRegisteringPostProcessor.class);
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
context.register(EarlyBeanInitializationDetector.class);
context.register(EagerInitBean.class);
assertThatNoException().isThrownBy(context::refresh);
}
@Test
void allowReplaceDefinitionWhenSingletonDefinitionPresent() {
this.parser.parse(SingletonBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
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);
assertThat(context.isSingleton("singleton")).as("isSingleton").isTrue();
assertThat(context.getBean("singleton")).as("overridden").isEqualTo("USED THIS");
}
@Test
void copyDefinitionPrimaryAndScope() {
this.parser.parse(SingletonBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
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);
assertThat(context.getBeanDefinition("singleton"))
.isNotSameAs(definition)
.matches(BeanDefinition::isPrimary, "isPrimary")
.satisfies(d -> assertThat(d.getScope()).isEqualTo("customScope"))
.matches(Predicate.not(BeanDefinition::isSingleton), "!isSingleton")
.matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype");
}
/*
Classes to parse and register with the bean post processor
-----
Note that some of these are both a @Configuration class and bean override field holder.
This is for this test convenience, as typically the bean override annotated fields
should not be in configuration classes but rather in test case classes
(where a TestExecutionListener automatically discovers and parses them).
*/
static final SomeInterface OVERRIDE = new SomeImplementation();
static final ExampleService OVERRIDE_SERVICE = new FailingExampleService();
static class ReplaceBeans {
@ExampleBeanOverrideAnnotation(value = "useThis", beanName = "explicit")
private ExampleService explicitName;
@ExampleBeanOverrideAnnotation(value = "useThis")
private ExampleService implicitName;
static ExampleService useThis() {
return OVERRIDE_SERVICE;
}
}
static class CreateIfOriginalIsMissingBean {
@ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true, beanName = "explicit")
private ExampleService explicitName;
@ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true)
private ExampleService implicitName;
static ExampleService useThis() {
return OVERRIDE_SERVICE;
}
}
@Configuration(proxyBeanMethods = false)
static class OverriddenFactoryBean {
@ExampleBeanOverrideAnnotation(value = "fOverride", beanName = "beanToBeOverridden")
SomeInterface f;
static SomeInterface fOverride() {
return OVERRIDE;
}
@Bean
TestFactoryBean testFactoryBean() {
return new TestFactoryBean();
}
}
static class EagerInitBean {
@ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true)
private ExampleService service;
static ExampleService useThis() {
return OVERRIDE_SERVICE;
}
}
static class SingletonBean {
@ExampleBeanOverrideAnnotation(beanName = "singleton",
value = "useThis", createIfMissing = false)
private String value;
static String useThis() {
return "USED THIS";
}
}
static class TestFactoryBean implements FactoryBean<Object> {
@Override
public Object getObject() {
return new SomeImplementation();
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return true;
}
}
static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(TestFactoryBean.class);
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("test", beanDefinition);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
static class EarlyBeanInitializationDetector implements BeanFactoryPostProcessor {
@Override
@SuppressWarnings("unchecked")
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
Map<String, BeanWrapper> cache = (Map<String, BeanWrapper>) ReflectionTestUtils.getField(beanFactory,
"factoryBeanInstanceCache");
Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered.");
}
}
interface SomeInterface {
}
static class SomeImplementation implements SomeInterface {
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.bean.override;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation;
import org.springframework.test.bean.override.example.TestBeanOverrideMetaAnnotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.springframework.test.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER;
class BeanOverrideParserTests {
@Test
void findsOnField() {
BeanOverrideParser parser = new BeanOverrideParser();
parser.parse(OnFieldConf.class);
assertThat(parser.getOverrideMetadata()).hasSize(1)
.first()
.extracting(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
.isEqualTo("onField");
}
@Test
void allowMultipleProcessorsOnDifferentElements() {
BeanOverrideParser parser = new BeanOverrideParser();
parser.parse(MultipleFieldsWithOnFieldConf.class);
assertThat(parser.getOverrideMetadata())
.hasSize(2)
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
.containsOnly("onField1", "onField2");
}
@Test
void rejectsMultipleAnnotationsOnSameElement() {
BeanOverrideParser parser = new BeanOverrideParser();
assertThatRuntimeException().isThrownBy(() -> parser.parse(MultipleOnFieldConf.class))
.withMessage("Multiple bean override annotations found on annotated field <" +
String.class.getName() + " " + MultipleOnFieldConf.class.getName() + ".message>");
}
@Test
void detectsDuplicateMetadata() {
BeanOverrideParser parser = new BeanOverrideParser();
assertThatRuntimeException().isThrownBy(() -> parser.parse(DuplicateConf.class))
.withMessage("Duplicate test overrideMetadata {DUPLICATE_TRIGGER}");
}
@Configuration
static class OnFieldConf {
@ExampleBeanOverrideAnnotation("onField")
String message;
static String onField() {
return "OK";
}
}
@Configuration
static class MultipleOnFieldConf {
@ExampleBeanOverrideAnnotation("foo")
@TestBeanOverrideMetaAnnotation
String message;
static String foo() {
return "foo";
}
}
@Configuration
static class MultipleFieldsWithOnFieldConf {
@ExampleBeanOverrideAnnotation("onField1")
String message;
@ExampleBeanOverrideAnnotation("onField2")
String messageOther;
static String onField1() {
return "OK1";
}
static String onField2() {
return "OK2";
}
}
@Configuration
static class DuplicateConf {
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
String message1;
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
String message2;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
class OverrideMetadataTests {
static class ConcreteOverrideMetadata extends OverrideMetadata {
ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride,
BeanOverrideStrategy strategy) {
super(field, overrideAnnotation, typeToOverride, strategy);
}
@Override
public String getBeanOverrideDescription() {
return ConcreteOverrideMetadata.class.getSimpleName();
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
return BeanOverrideStrategy.REPLACE_DEFINITION;
}
}
@NonNull
public String annotated = "exampleField";
static OverrideMetadata exampleOverride() throws NoSuchFieldException {
final Field annotated = OverrideMetadataTests.class.getField("annotated");
return new ConcreteOverrideMetadata(Objects.requireNonNull(annotated), annotated.getAnnotation(NonNull.class),
ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION);
}
@Test
void implicitConfigurations() throws NoSuchFieldException {
final OverrideMetadata metadata = exampleOverride();
assertThat(metadata.getExpectedBeanName()).as("expectedBeanName")
.isEqualTo(metadata.field().getName());
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.bean.override.convention;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.bean.override.example.ExampleService;
import org.springframework.test.bean.override.example.FailingExampleService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
class TestBeanOverrideProcessorTests {
@Test
void ensureMethodFindsFromList() {
Method m = TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class,
"example1", "example2", "example3");
assertThat(m.getName()).isEqualTo("example2");
}
@Test
void ensureMethodNotFound() {
assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(
MethodConventionConf.class, ExampleService.class, "example1", "example3"))
.withMessage("Found 0 static methods instead of exactly one, matching a name in [example1, example3] with return type " +
ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName())
.isInstanceOf(IllegalStateException.class);
}
@Test
void ensureMethodTwoFound() {
assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(
MethodConventionConf.class, ExampleService.class, "example2", "example4"))
.withMessage("Found 2 static methods instead of exactly one, matching a name in [example2, example4] with return type " +
ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName())
.isInstanceOf(IllegalStateException.class);
}
@Test
void ensureMethodNoNameProvided() {
assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(
MethodConventionConf.class, ExampleService.class))
.withMessage("At least one expectedMethodName is required")
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void createMetaDataForUnknownExplicitMethod() throws NoSuchFieldException {
Field f = ExplicitMethodNameConf.class.getField("a");
final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class));
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThatException().isThrownBy(() -> processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class)))
.withMessage("Found 0 static methods instead of exactly one, matching a name in [explicit1] with return type " +
ExampleService.class.getName() + " on class " + ExplicitMethodNameConf.class.getName())
.isInstanceOf(IllegalStateException.class);
}
@Test
void createMetaDataForKnownExplicitMethod() throws NoSuchFieldException {
Field f = ExplicitMethodNameConf.class.getField("b");
final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class));
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class)))
.isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class);
}
@Test
void createMetaDataWithDeferredEnsureMethodCheck() throws NoSuchFieldException {
Field f = MethodConventionConf.class.getField("field");
final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class));
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class)))
.isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class);
}
static class MethodConventionConf {
@TestBean
public ExampleService field;
@Bean
ExampleService example1() {
return new FailingExampleService();
}
static ExampleService example2() {
return new FailingExampleService();
}
public static ExampleService example4() {
return new FailingExampleService();
}
}
static class ExplicitMethodNameConf {
@TestBean(methodName = "explicit1")
public ExampleService a;
@TestBean(methodName = "explicit2")
public ExampleService b;
static ExampleService explicit2() {
return new FailingExampleService();
}
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.bean.override.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.test.bean.override.BeanOverride;
@BeanOverride(ExampleBeanOverrideProcessor.class)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExampleBeanOverrideAnnotation {
static final String DEFAULT_VALUE = "TEST OVERRIDE";
String value() default DEFAULT_VALUE;
boolean createIfMissing() default false;
String beanName() default "";
}

View File

@ -0,0 +1,49 @@
/*
* 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.bean.override.example;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.springframework.core.ResolvableType;
import org.springframework.test.bean.override.BeanOverrideProcessor;
import org.springframework.test.bean.override.OverrideMetadata;
public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor {
public ExampleBeanOverrideProcessor() {
}
private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() {
@Override
public String toString() {
return "{DUPLICATE_TRIGGER}";
}
};
public static final String DUPLICATE_TRIGGER = "CONSTANT";
@Override
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) {
throw new IllegalStateException("unexpected annotation");
}
if (annotation.value().equals(DUPLICATE_TRIGGER)) {
return CONSTANT;
}
return new TestOverrideMetadata(field, annotation, typeToOverride);
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.bean.override.example;
/**
* Example service interface for mocking tests.
*
* @author Phillip Webb
*/
public interface ExampleService {
String greeting();
}

View File

@ -0,0 +1,34 @@
/*
* 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.bean.override.example;
import org.springframework.stereotype.Service;
/**
* An {@link ExampleService} that always throws an exception.
*
* @author Phillip Webb
*/
@Service
public class FailingExampleService implements ExampleService {
@Override
public String greeting() {
throw new IllegalStateException("Failed");
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.bean.override.example;
/**
* Example service implementation for spy tests.
*
* @author Phillip Webb
*/
public class RealExampleService implements ExampleService {
private final String greeting;
public RealExampleService(String greeting) {
this.greeting = greeting;
}
@Override
public String greeting() {
return this.greeting;
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.bean.override.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExampleBeanOverrideAnnotation("foo")
public @interface TestBeanOverrideMetaAnnotation { }

View File

@ -0,0 +1,119 @@
/*
* 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.bean.override.example;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.test.bean.override.BeanOverrideStrategy;
import org.springframework.test.bean.override.OverrideMetadata;
import org.springframework.util.StringUtils;
import static org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE;
public class TestOverrideMetadata extends OverrideMetadata {
@Nullable
private final Method method;
@Nullable
private final String beanName;
@Nullable
private static Method findMethod(AnnotatedElement element, String methodName) {
if (DEFAULT_VALUE.equals(methodName)) {
return null;
}
if (element instanceof Field f) {
for (Method m : f.getDeclaringClass().getDeclaredMethods()) {
if (!Modifier.isStatic(m.getModifiers())) {
continue;
}
if (m.getName().equals(methodName)) {
return m;
}
}
throw new IllegalStateException("Expected a static method named <" + methodName + "> alongside annotated field <" + f.getName() + ">");
}
if (element instanceof Method m) {
if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) {
return m;
}
throw new IllegalStateException("Expected the annotated method to be static and named <" + methodName + ">");
}
if (element instanceof Class c) {
for (Method m : c.getDeclaredMethods()) {
if (!Modifier.isStatic(m.getModifiers())) {
continue;
}
if (m.getName().equals(methodName)) {
return m;
}
}
throw new IllegalStateException("Expected a static method named <" + methodName + "> on annotated class <" + c.getSimpleName() + ">");
}
throw new IllegalStateException("Expected the annotated element to be a Field, Method or Class");
}
public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) {
super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ?
BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION);
this.method = findMethod(field, overrideAnnotation.value());
this.beanName = overrideAnnotation.beanName();
}
//Used to trigger duplicate detection in parser test
TestOverrideMetadata() {
super(null, null, null, null);
this.method = null;
this.beanName = null;
}
@Override
protected String getExpectedBeanName() {
if (StringUtils.hasText(this.beanName)) {
return this.beanName;
}
return super.getExpectedBeanName();
}
@Override
public String getBeanOverrideDescription() {
return "test";
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
if (this.method == null) {
return DEFAULT_VALUE;
}
try {
this.method.setAccessible(true);
return this.method.invoke(null);
}
catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,9 @@
/**
* Example components for testing spring-test Bean overriding feature.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.bean.override.example;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -25,6 +25,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.test.bean.override.BeanOverrideTestExecutionListener;
import org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener;
import org.springframework.test.bean.override.mockito.MockitoTestExecutionListener;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
@ -65,12 +68,15 @@ class TestExecutionListenersTests {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
MockitoTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
micrometerListenerClass,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
EventPublishingTestExecutionListener.class,//
MockitoResetTestExecutionListener.class,//
BeanOverrideTestExecutionListener.class
);
assertRegisteredListeners(DefaultListenersTestCase.class, expected);
}
@ -84,12 +90,15 @@ class TestExecutionListenersTests {
ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
MockitoTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
micrometerListenerClass,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
EventPublishingTestExecutionListener.class,//
MockitoResetTestExecutionListener.class,//
BeanOverrideTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerPrependedTestCase.class, expected);
}
@ -102,12 +111,15 @@ class TestExecutionListenersTests {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
MockitoTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
micrometerListenerClass,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
MockitoResetTestExecutionListener.class,//
BeanOverrideTestExecutionListener.class,//
BazTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerAppendedTestCase.class, expected);
@ -121,13 +133,16 @@ class TestExecutionListenersTests {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
MockitoTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
BarTestExecutionListener.class,//
micrometerListenerClass,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
EventPublishingTestExecutionListener.class,//
MockitoResetTestExecutionListener.class,//
BeanOverrideTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerInsertedTestCase.class, expected);
}