Improve Bean Overriding support, testing and documentation

This commit improves on the bean overriding feature in several ways:
the API is simplified and polished (metadata and processor contracts,
 etc...).

The commit also reworks infrastructure classes (context customizer,
test execution listener, BeanOverrideBeanFactoryPostProcessor, etc...).
Parsing of annotations is now fully stateless.

In order to avoid OverrideMetadata in bean definition and to make a
first step towards AOT support, the BeanOverrideBeanFactoryPostProcessor
now delegates to a BeanOverrideRegistrar to track classes to parse,
the metadata-related state as well as for the field injection methods
for tests.

Lastly, this commit increases the test coverage for the provided
annotations and adds integration tests and fixes a few `@TestBean`
issues.
This commit is contained in:
Simon Baslé
2024-03-20 11:48:22 +01:00
parent 711ddd1ac6
commit 2d33aac350
30 changed files with 1714 additions and 820 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.test.context.bean.override;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
@@ -46,20 +47,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for for {@link BeanOverrideBeanPostProcessor}.
* Tests for {@link BeanOverrideBeanFactoryPostProcessor} combined with a
* {@link BeanOverrideRegistrar}.
*
* @author Simon Baslé
*/
class BeanOverrideBeanPostProcessorTests {
private final BeanOverrideParser parser = new BeanOverrideParser();
class BeanOverrideBeanFactoryPostProcessorTests {
@Test
void canReplaceExistingBeanDefinitions() {
this.parser.parse(ReplaceBeans.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
context.register(ReplaceBeans.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
@@ -72,22 +69,19 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void cannotReplaceIfNoBeanMatching() {
this.parser.parse(ReplaceBeans.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
context.register(ReplaceBeans.class);
//note we don't register any original bean here
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'");
.withMessage("Unable to override bean 'explicit'; " +
"there is no bean definition to replace with that name");
}
@Test
void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() {
this.parser.parse(CreateIfOriginalIsMissingBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
context.register(CreateIfOriginalIsMissingBean.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
@@ -100,9 +94,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canCreateIfOriginalMissingWithCreateReplaceStrategy() {
this.parser.parse(CreateIfOriginalIsMissingBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
context.register(CreateIfOriginalIsMissingBean.class);
//note we don't register original beans here
@@ -114,9 +106,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() {
this.parser.parse(OverriddenFactoryBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class);
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
@@ -129,9 +119,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() {
this.parser.parse(OverriddenFactoryBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
ResolvableType objectType = ResolvableType.forClass(SomeInterface.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType);
@@ -145,10 +133,9 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void postProcessorShouldNotTriggerEarlyInitialization() {
this.parser.parse(EagerInitBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext context = createContext(EagerInitBean.class);
context.register(FactoryBeanRegisteringPostProcessor.class);
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
context.register(EarlyBeanInitializationDetector.class);
context.register(EagerInitBean.class);
@@ -157,12 +144,10 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void allowReplaceDefinitionWhenSingletonDefinitionPresent() {
this.parser.parse(SingletonBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
definition.setScope(BeanDefinition.SCOPE_SINGLETON);
context.registerBeanDefinition("singleton", definition);
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(SingletonBean.class);
assertThatNoException().isThrownBy(context::refresh);
@@ -172,14 +157,12 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void copyDefinitionPrimaryAndScope() {
this.parser.parse(SingletonBean.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
context.getBeanFactory().registerScope("customScope", new SimpleThreadScope());
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
definition.setScope("customScope");
definition.setPrimary(true);
context.registerBeanDefinition("singleton", definition);
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(SingletonBean.class);
assertThatNoException().isThrownBy(context::refresh);
@@ -192,6 +175,14 @@ class BeanOverrideBeanPostProcessorTests {
}
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideRegistrar.register(context, Set.of(classes));
BeanOverrideBeanFactoryPostProcessor.register(context);
return context;
}
/*
Classes to parse and register with the bean post processor
-----

View File

@@ -28,33 +28,27 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
/**
* Unit tests for {@link BeanOverrideParser}.
* Unit tests for {@link BeanOverrideParsingUtils}.
*
* @since 6.2
*/
class BeanOverrideParserTests {
// Copy of ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER which is package-private.
private static final String DUPLICATE_TRIGGER = "DUPLICATE";
private final BeanOverrideParser parser = new BeanOverrideParser();
class BeanOverrideParsingUtilsTests {
// Metadata built from a String that starts with DUPLICATE_TRIGGER are considered equal
private static final String DUPLICATE_TRIGGER1 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v1";
private static final String DUPLICATE_TRIGGER2 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v2";
@Test
void findsOnField() {
parser.parse(SingleAnnotationOnField.class);
assertThat(parser.getOverrideMetadata())
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
assertThat(BeanOverrideParsingUtils.parse(SingleAnnotationOnField.class))
.map(Object::toString)
.containsExactly("onField");
}
@Test
void allowsMultipleProcessorsOnDifferentElements() {
parser.parse(AnnotationsOnMultipleFields.class);
assertThat(parser.getOverrideMetadata())
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
assertThat(BeanOverrideParsingUtils.parse(AnnotationsOnMultipleFields.class))
.map(Object::toString)
.containsExactlyInAnyOrder("onField1", "onField2");
}
@@ -62,15 +56,15 @@ class BeanOverrideParserTests {
void rejectsMultipleAnnotationsOnSameElement() {
Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message");
assertThatRuntimeException()
.isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class))
.isThrownBy(() -> BeanOverrideParsingUtils.parse(MultipleAnnotationsOnField.class))
.withMessage("Multiple @BeanOverride annotations found on field: " + field);
}
@Test
void detectsDuplicateMetadata() {
assertThatRuntimeException()
.isThrownBy(() -> parser.parse(DuplicateConf.class))
.withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}");
void keepsFirstOccurrenceOfEqualMetadata() {
assertThat(BeanOverrideParsingUtils.parse(DuplicateConf.class))
.map(Object::toString)
.containsExactly("{DUPLICATE-v1}");
}
@@ -114,10 +108,10 @@ class BeanOverrideParserTests {
static class DuplicateConf {
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER1)
String message1;
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER2)
String message2;
}

View File

@@ -16,7 +16,6 @@
package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
@@ -39,7 +38,7 @@ class OverrideMetadataTests {
@Test
void implicitConfigurations() throws Exception {
OverrideMetadata metadata = exampleOverride();
assertThat(metadata.getExpectedBeanName()).as("expectedBeanName").isEqualTo(metadata.field().getName());
assertThat(metadata.getBeanName()).as("expectedBeanName").isEqualTo(metadata.getField().getName());
}
@@ -48,21 +47,16 @@ class OverrideMetadataTests {
private static OverrideMetadata exampleOverride() throws Exception {
Field field = OverrideMetadataTests.class.getDeclaredField("annotated");
return new ConcreteOverrideMetadata(field, field.getAnnotation(NonNull.class),
ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION);
return new ConcreteOverrideMetadata(field, ResolvableType.forClass(String.class),
BeanOverrideStrategy.REPLACE_DEFINITION);
}
static class ConcreteOverrideMetadata extends OverrideMetadata {
ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride,
ConcreteOverrideMetadata(Field field, ResolvableType typeToOverride,
BeanOverrideStrategy strategy) {
super(field, overrideAnnotation, typeToOverride, strategy);
}
@Override
public String getBeanOverrideDescription() {
return ConcreteOverrideMetadata.class.getSimpleName();
super(field, typeToOverride, strategy);
}
@Override

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.convention;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig
class AbstractTestBeanIntegrationTestCase {
@TestBean
Pojo someBean;
@TestBean
Pojo otherBean;
@TestBean(name = "thirdBean")
Pojo anotherBean;
static Pojo otherBeanTestOverride() {
return new FakePojo("otherBean in superclass");
}
static Pojo thirdBeanTestOverride() {
return new FakePojo("third in superclass");
}
static Pojo commonBeanOverride() {
return new FakePojo("in superclass");
}
interface Pojo {
default String getValue() {
return "Prod";
}
}
static class ProdPojo implements Pojo { }
static class FakePojo implements Pojo {
final String value;
protected FakePojo(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
@Override
public String toString() {
return getValue();
}
}
@Configuration
static class Config {
@Bean
Pojo someBean() {
return new ProdPojo();
}
@Bean
Pojo otherBean() {
return new ProdPojo();
}
@Bean
Pojo thirdBean() {
return new ProdPojo();
}
@Bean
Pojo pojo() {
return new ProdPojo();
}
@Bean
Pojo pojo2() {
return new ProdPojo();
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.convention;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.context.ApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
public class TestBeanInheritanceIntegrationTests {
static AbstractTestBeanIntegrationTestCase.Pojo nestedBeanOverride() {
return new AbstractTestBeanIntegrationTestCase.FakePojo("in enclosing test class");
}
@Nested
@DisplayName("Concrete inherited test with correct @TestBean setup")
class ConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
@TestBean(methodName = "commonBeanOverride")
Pojo pojo;
@TestBean(methodName = "nestedBeanOverride")
Pojo pojo2;
static Pojo someBeanTestOverride() {
return new FakePojo("someBeanOverride");
}
@Test
void fieldInSupertypeMethodInType(ApplicationContext ctx) {
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
}
@Test
void fieldInTypeMethodInSuperType(ApplicationContext ctx) {
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
}
@Test
void fieldInTypeMethodInEnclosingClass(ApplicationContext ctx) {
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
}
@Test
void fieldInSupertypePrioritizeMethodInType(ApplicationContext ctx) {
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
}
}
@Test
void failsIfFieldInSupertypeButNoMethod() {
Class<?> clazz = Failing1.class;
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(clazz))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.rootCause().isInstanceOf(IllegalStateException.class)
.hasMessage("""
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates [someBeanTestOverride]""",
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
}
@Test
void failsIfMethod1InSupertypeAndMethod2InType() {
Class<?> clazz = Failing2.class;
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(clazz))
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.rootCause().isInstanceOf(IllegalStateException.class)
.hasMessage("""
Found 2 competing static test bean factory methods in %s with return type %s \
whose name matches one of the supported candidates \
[thirdBeanTestOverride, anotherBeanTestOverride]""",
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
}
static class Failing1 extends AbstractTestBeanIntegrationTestCase {
@Test
void ignored() {
}
}
static class Failing2 extends AbstractTestBeanIntegrationTestCase {
static Pojo someBeanTestOverride() {
return new FakePojo("ignored");
}
static Pojo anotherBeanTestOverride() {
return new FakePojo("sub2");
}
@Test
void ignored2() { }
}
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.convention;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@SpringJUnitConfig
public class TestBeanIntegrationTests {
@TestBean
String field;
@TestBean
String nestedField;
@TestBean(name = "field")
String renamed1;
@TestBean(name = "nestedField")
String renamed2;
@TestBean(methodName = "fieldTestOverride")
String methodRenamed1;
@TestBean(methodName = "nestedFieldTestOverride")
String methodRenamed2;
static String fieldTestOverride() {
return "fieldOverride";
}
static String nestedFieldTestOverride() {
return "nestedFieldOverride";
}
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
assertThat(this.field).as("injection point").isEqualTo("fieldOverride");
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride");
}
@Test
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride");
assertThat(this.methodRenamed1).as("injection point").isEqualTo("fieldOverride");
}
@Test
void testBeanFailingNoFieldNameBean() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failing1.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'noOriginalBean'; " +
"there is no bean definition to replace with that name"));
}
@Test
void testBeanFailingNoExplicitNameBean() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failing2.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'notPresent'; " +
"there is no bean definition to replace with that name"));
}
@Test
void testBeanFailingNoImplicitMethod() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failing3.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.rootCause().isInstanceOf(IllegalStateException.class)
.hasMessage("Failed to find a static test bean factory method in " +
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing3 " +
"with return type java.lang.String whose name matches one of the " +
"supported candidates [notPresent]"));
}
@Test
void testBeanFailingNoExplicitMethod() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failing4.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.rootCause().isInstanceOf(IllegalStateException.class)
.hasMessage("Failed to find a static test bean factory method in " +
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing4 " +
"with return type java.lang.String whose name matches one of the " +
"supported candidates [fieldTestOverride]"));
}
@Nested
@DisplayName("With @TestBean on enclosing class")
class TestBeanNested {
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
assertThat(TestBeanIntegrationTests.this.nestedField).isEqualTo("nestedFieldOverride");
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
assertThat(TestBeanIntegrationTests.this.renamed2).isEqualTo("nestedFieldOverride");
}
@Test
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride");
assertThat(TestBeanIntegrationTests.this.methodRenamed2).isEqualTo("nestedFieldOverride");
}
}
@Nested
@DisplayName("With factory method on enclosing class")
class TestBeanNested2 {
@TestBean(methodName = "nestedFieldTestOverride", name = "nestedField")
String nestedField2;
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
assertThat(this.nestedField2).isEqualTo("nestedFieldOverride");
}
}
@Configuration
static class Config {
@Bean("field")
String bean1() {
return "prod";
}
@Bean("nestedField")
String bean2() {
return "nestedProd";
}
@Bean("methodRenamed1")
String bean3() {
return "Prod";
}
@Bean("methodRenamed2")
String bean4() {
return "NestedProd";
}
}
@SpringJUnitConfig
static class Failing1 {
@TestBean
String noOriginalBean;
@Test
void ignored() {
fail("should fail earlier");
}
static String noOriginalBeanTestOverride() {
return "should be ignored";
}
}
@SpringJUnitConfig
static class Failing2 {
@TestBean(name = "notPresent")
String field;
@Test
void ignored() {
fail("should fail earlier");
}
static String notPresentTestOverride() {
return "should be ignored";
}
}
@SpringJUnitConfig
static class Failing3 {
@TestBean(methodName = "notPresent")
String field;
@Test
void ignored() {
fail("should fail earlier");
}
}
@SpringJUnitConfig
static class Failing4 {
@TestBean //expects fieldTestOverride method
String field;
@Test
void ignored() {
fail("should fail earlier");
}
}
}

View File

@@ -23,8 +23,9 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.MethodConventionOverrideMetadata;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.TestBeanOverrideMetadata;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.FailingExampleService;
@@ -95,7 +96,7 @@ class TestBeanOverrideProcessorTests {
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThatIllegalStateException()
.isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
.isThrownBy(() -> processor.createMetadata(overrideAnnotation, clazz, field))
.withMessage("""
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""",
@@ -104,35 +105,49 @@ class TestBeanOverrideProcessorTests {
@Test
void createMetaDataForKnownExplicitMethod() throws Exception {
Class<?> returnType = ExampleService.class;
Field field = ExplicitMethodNameConf.class.getField("b");
Class<?> clazz = ExplicitMethodNameConf.class;
Field field = clazz.getField("b");
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
assertThat(overrideAnnotation).isNotNull();
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
.isInstanceOf(MethodConventionOverrideMetadata.class);
assertThat(processor.createMetadata(overrideAnnotation, clazz, field))
.isInstanceOf(TestBeanOverrideMetadata.class);
}
@Test
void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception {
void createMetaDataForConventionBasedFactoryMethod() throws Exception {
Class<?> returnType = ExampleService.class;
Field field = MethodConventionConf.class.getField("field");
Class<?> clazz = MethodConventionConf.class;
Field field = clazz.getField("field");
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
assertThat(overrideAnnotation).isNotNull();
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
// When in convention-based mode, createMetadata() will not verify that
// the factory method actually exists. So, we don't expect an exception
// for this use case.
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
.isInstanceOf(MethodConventionOverrideMetadata.class);
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(
overrideAnnotation, clazz, field))
.withMessage("""
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""",
clazz.getName(), returnType.getName(), List.of("someFieldTestOverride", "fieldTestOverride"));
}
@Test
void failToCreateMetadataForOtherAnnotation() throws NoSuchFieldException {
Class<?> clazz = MethodConventionConf.class;
Field field = clazz.getField("field");
NonNull badAnnotation = AnnotationUtils.synthesizeAnnotation(NonNull.class);
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(badAnnotation, clazz, field))
.withMessage("Invalid annotation passed to TestBeanOverrideProcessor: expected @TestBean" +
" on field %s.%s", field.getDeclaringClass().getName(), field.getName());
}
static class MethodConventionConf {
@TestBean
@TestBean(name = "someField")
public ExampleService field;
@Bean

View File

@@ -29,6 +29,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface ExampleBeanOverrideAnnotation {
String DEFAULT_VALUE = "TEST OVERRIDE";
// Any metadata using this as the prefix for the bean name will be considered equal
String DUPLICATE_TRIGGER = "DUPLICATE";
String value() default DEFAULT_VALUE;

View File

@@ -26,24 +26,15 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
// Intentionally NOT public
class ExampleBeanOverrideProcessor implements BeanOverrideProcessor {
static final String DUPLICATE_TRIGGER = "DUPLICATE";
private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() {
@Override
public String toString() {
return "{DUPLICATE_TRIGGER}";
}
};
@Override
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) {
throw new IllegalStateException("unexpected annotation");
}
if (annotation.value().equals(DUPLICATE_TRIGGER)) {
return CONSTANT;
if (annotation.value().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER)) {
return new TestOverrideMetadata(annotation.value());
}
return new TestOverrideMetadata(field, annotation, typeToOverride);
return new TestOverrideMetadata(field, annotation, ResolvableType.forField(field, testClass));
}
}

View File

@@ -40,6 +40,8 @@ class TestOverrideMetadata extends OverrideMetadata {
@Nullable
private final String beanName;
private final String methodName;
@Nullable
private static Method findMethod(AnnotatedElement element, String methodName) {
if (DEFAULT_VALUE.equals(methodName)) {
@@ -77,30 +79,31 @@ class TestOverrideMetadata extends OverrideMetadata {
}
public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) {
super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ?
super(field, typeToOverride, overrideAnnotation.createIfMissing() ?
BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION);
this.method = findMethod(field, overrideAnnotation.value());
this.methodName = overrideAnnotation.value();
this.beanName = overrideAnnotation.beanName();
}
//Used to trigger duplicate detection in parser test
TestOverrideMetadata() {
super(null, null, null, null);
TestOverrideMetadata(String duplicateTrigger) {
super(null, null, null);
this.method = null;
this.beanName = null;
this.methodName = duplicateTrigger;
this.beanName = duplicateTrigger;
}
@Override
protected String getExpectedBeanName() {
protected String getBeanName() {
if (StringUtils.hasText(this.beanName)) {
return this.beanName;
}
return super.getExpectedBeanName();
return super.getBeanName();
}
@Override
public String getBeanOverrideDescription() {
return "test";
String getAnnotationMethodName() {
return this.methodName;
}
@Override
@@ -117,4 +120,29 @@ class TestOverrideMetadata extends OverrideMetadata {
}
}
@Override
public boolean equals(Object obj) {
if (this.method == null) {
return obj instanceof TestOverrideMetadata tem &&
tem.beanName != null &&
tem.beanName.startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER);
}
return super.equals(obj);
}
@Override
public int hashCode() {
if (this.method == null) {
return ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER.hashCode();
}
return super.hashCode();
}
@Override
public String toString() {
if (this.method == null) {
return "{" + this.beanName + "}";
}
return this.methodName;
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig
public class MockitoBeanIntegrationTests {
@MockitoBean
ExampleService field;
@MockitoBean
ExampleService nestedField;
@MockitoBean(name = "field")
ExampleService renamed1;
@MockitoBean(name = "nestedField")
ExampleService renamed2;
@MockitoBean(name = "nonExistingBean")
ExampleService nonExisting1;
@MockitoBean(name = "nestedNonExistingBean")
ExampleService nonExisting2;
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(this.field);
assertThat(this.field.greeting()).as("mocked greeting").isNull();
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(this.renamed1);
assertThat(this.renamed1.greeting()).as("mocked greeting").isNull();
}
@Test
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
assertThat(ctx.getBean("nonExistingBean"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(this.nonExisting1);
assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull();
}
@Nested
@DisplayName("With @MockitoBean on enclosing class")
class MockitoBeanNested {
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(MockitoBeanIntegrationTests.this.nestedField);
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(MockitoBeanIntegrationTests.this.renamed2);
}
@Test
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedNonExistingBean"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(MockitoBeanIntegrationTests.this.nonExisting2);
}
}
@Configuration
static class Config {
@Bean("field")
ExampleService bean1() {
return new RealExampleService("Hello Field");
}
@Bean("nestedField")
ExampleService bean2() {
return new RealExampleService("Hello Nested Field");
}
}
}

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.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.test.context.bean.override.OverrideMetadata;
import org.springframework.test.context.bean.override.example.ExampleService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
public class MockitoBeanOverrideProcessorTests {
@Test
void mockAnnotationCreatesMockMetadata() throws NoSuchFieldException {
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
assertThat(object).isExactlyInstanceOf(MockitoBeanMetadata.class);
}
@Test
void spyAnnotationCreatesSpyMetadata() throws NoSuchFieldException {
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanMetadata.class);
}
@Test
void otherAnnotationThrows() throws NoSuchFieldException {
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
Annotation annotation = field.getAnnotation(Nullable.class);
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(annotation, clazz, field))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected " +
"@MockitoBean/@MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
field.getName());
}
static class MockitoConf {
@Nullable
@MockitoBean
@MockitoSpyBean
public ExampleService a;
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@SpringJUnitConfig(MockitoBeanIntegrationTests.Config.class)
public class MockitoSpyBeanIntegrationTests {
@MockitoSpyBean
ExampleService field;
@MockitoSpyBean
ExampleService nestedField;
@MockitoSpyBean(name = "field")
ExampleService renamed1;
@MockitoSpyBean(name = "nestedField")
ExampleService renamed2;
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(this.field);
assertThat(this.field.greeting()).as("spied greeting")
.isEqualTo("Hello Field");
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(this.renamed1);
assertThat(this.field.greeting()).as("spied greeting")
.isEqualTo("Hello Field");
}
@Test
void failWhenBeanNotPresentFieldName() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failure1.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'notPresent' by wrapping," +
" no existing bean instance by this name of type %s",
ExampleService.class.getName()));
}
@Test
void failWhenBeanNotPresentExplicitName() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failure2.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'notPresentAtAll' by wrapping," +
" no existing bean instance by this name of type %s",
ExampleService.class.getName()));
}
@Nested
@DisplayName("With @MockitoSpyBean on enclosing class")
class MockitoBeanNested {
@Test
void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(MockitoSpyBeanIntegrationTests.this.nestedField);
assertThat(MockitoSpyBeanIntegrationTests.this.nestedField.greeting())
.as("spied greeting")
.isEqualTo("Hello Nested Field");
}
@Test
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("nestedField"))
.isInstanceOf(ExampleService.class)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(MockitoSpyBeanIntegrationTests.this.renamed2);
assertThat(MockitoSpyBeanIntegrationTests.this.renamed2.greeting())
.as("spied greeting")
.isEqualTo("Hello Nested Field");
}
}
@SpringJUnitConfig
static class Failure1 {
@MockitoSpyBean
ExampleService notPresent;
@Test
void ignored() { }
}
@SpringJUnitConfig
static class Failure2 {
@MockitoSpyBean(name = "notPresentAtAll")
ExampleService field;
@Test
void ignored() { }
}
}