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

@@ -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);
}