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:
@@ -17,6 +17,7 @@
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -46,20 +47,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for for {@link BeanOverrideBeanPostProcessor}.
|
||||
* Tests for {@link BeanOverrideBeanFactoryPostProcessor} combined with a
|
||||
* {@link BeanOverrideRegistrar}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
private final BeanOverrideParser parser = new BeanOverrideParser();
|
||||
|
||||
class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void canReplaceExistingBeanDefinitions() {
|
||||
this.parser.parse(ReplaceBeans.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
|
||||
context.register(ReplaceBeans.class);
|
||||
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
@@ -72,22 +69,19 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void cannotReplaceIfNoBeanMatching() {
|
||||
this.parser.parse(ReplaceBeans.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
|
||||
context.register(ReplaceBeans.class);
|
||||
//note we don't register any original bean here
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'");
|
||||
.withMessage("Unable to override bean 'explicit'; " +
|
||||
"there is no bean definition to replace with that name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() {
|
||||
this.parser.parse(CreateIfOriginalIsMissingBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
|
||||
context.register(CreateIfOriginalIsMissingBean.class);
|
||||
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
|
||||
@@ -100,9 +94,7 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void canCreateIfOriginalMissingWithCreateReplaceStrategy() {
|
||||
this.parser.parse(CreateIfOriginalIsMissingBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
|
||||
context.register(CreateIfOriginalIsMissingBean.class);
|
||||
//note we don't register original beans here
|
||||
|
||||
@@ -114,9 +106,7 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() {
|
||||
this.parser.parse(OverriddenFactoryBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
|
||||
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class);
|
||||
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
|
||||
@@ -129,9 +119,7 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() {
|
||||
this.parser.parse(OverriddenFactoryBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
|
||||
ResolvableType objectType = ResolvableType.forClass(SomeInterface.class);
|
||||
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType);
|
||||
@@ -145,10 +133,9 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void postProcessorShouldNotTriggerEarlyInitialization() {
|
||||
this.parser.parse(EagerInitBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(EagerInitBean.class);
|
||||
|
||||
context.register(FactoryBeanRegisteringPostProcessor.class);
|
||||
BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
|
||||
context.register(EarlyBeanInitializationDetector.class);
|
||||
context.register(EagerInitBean.class);
|
||||
|
||||
@@ -157,12 +144,10 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void allowReplaceDefinitionWhenSingletonDefinitionPresent() {
|
||||
this.parser.parse(SingletonBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
|
||||
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
|
||||
definition.setScope(BeanDefinition.SCOPE_SINGLETON);
|
||||
context.registerBeanDefinition("singleton", definition);
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
context.register(SingletonBean.class);
|
||||
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
@@ -172,14 +157,12 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void copyDefinitionPrimaryAndScope() {
|
||||
this.parser.parse(SingletonBean.class);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
|
||||
context.getBeanFactory().registerScope("customScope", new SimpleThreadScope());
|
||||
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
|
||||
definition.setScope("customScope");
|
||||
definition.setPrimary(true);
|
||||
context.registerBeanDefinition("singleton", definition);
|
||||
BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
|
||||
context.register(SingletonBean.class);
|
||||
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
@@ -192,6 +175,14 @@ class BeanOverrideBeanPostProcessorTests {
|
||||
}
|
||||
|
||||
|
||||
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
BeanOverrideRegistrar.register(context, Set.of(classes));
|
||||
BeanOverrideBeanFactoryPostProcessor.register(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Classes to parse and register with the bean post processor
|
||||
-----
|
||||
@@ -28,33 +28,27 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BeanOverrideParser}.
|
||||
* Unit tests for {@link BeanOverrideParsingUtils}.
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
class BeanOverrideParserTests {
|
||||
|
||||
// Copy of ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER which is package-private.
|
||||
private static final String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
private final BeanOverrideParser parser = new BeanOverrideParser();
|
||||
class BeanOverrideParsingUtilsTests {
|
||||
|
||||
// Metadata built from a String that starts with DUPLICATE_TRIGGER are considered equal
|
||||
private static final String DUPLICATE_TRIGGER1 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v1";
|
||||
private static final String DUPLICATE_TRIGGER2 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v2";
|
||||
|
||||
@Test
|
||||
void findsOnField() {
|
||||
parser.parse(SingleAnnotationOnField.class);
|
||||
|
||||
assertThat(parser.getOverrideMetadata())
|
||||
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
|
||||
assertThat(BeanOverrideParsingUtils.parse(SingleAnnotationOnField.class))
|
||||
.map(Object::toString)
|
||||
.containsExactly("onField");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsMultipleProcessorsOnDifferentElements() {
|
||||
parser.parse(AnnotationsOnMultipleFields.class);
|
||||
|
||||
assertThat(parser.getOverrideMetadata())
|
||||
.map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
|
||||
assertThat(BeanOverrideParsingUtils.parse(AnnotationsOnMultipleFields.class))
|
||||
.map(Object::toString)
|
||||
.containsExactlyInAnyOrder("onField1", "onField2");
|
||||
}
|
||||
|
||||
@@ -62,15 +56,15 @@ class BeanOverrideParserTests {
|
||||
void rejectsMultipleAnnotationsOnSameElement() {
|
||||
Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message");
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class))
|
||||
.isThrownBy(() -> BeanOverrideParsingUtils.parse(MultipleAnnotationsOnField.class))
|
||||
.withMessage("Multiple @BeanOverride annotations found on field: " + field);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsDuplicateMetadata() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> parser.parse(DuplicateConf.class))
|
||||
.withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}");
|
||||
void keepsFirstOccurrenceOfEqualMetadata() {
|
||||
assertThat(BeanOverrideParsingUtils.parse(DuplicateConf.class))
|
||||
.map(Object::toString)
|
||||
.containsExactly("{DUPLICATE-v1}");
|
||||
}
|
||||
|
||||
|
||||
@@ -114,10 +108,10 @@ class BeanOverrideParserTests {
|
||||
|
||||
static class DuplicateConf {
|
||||
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER1)
|
||||
String message1;
|
||||
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
|
||||
@ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER2)
|
||||
String message2;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -39,7 +38,7 @@ class OverrideMetadataTests {
|
||||
@Test
|
||||
void implicitConfigurations() throws Exception {
|
||||
OverrideMetadata metadata = exampleOverride();
|
||||
assertThat(metadata.getExpectedBeanName()).as("expectedBeanName").isEqualTo(metadata.field().getName());
|
||||
assertThat(metadata.getBeanName()).as("expectedBeanName").isEqualTo(metadata.getField().getName());
|
||||
}
|
||||
|
||||
|
||||
@@ -48,21 +47,16 @@ class OverrideMetadataTests {
|
||||
|
||||
private static OverrideMetadata exampleOverride() throws Exception {
|
||||
Field field = OverrideMetadataTests.class.getDeclaredField("annotated");
|
||||
return new ConcreteOverrideMetadata(field, field.getAnnotation(NonNull.class),
|
||||
ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
return new ConcreteOverrideMetadata(field, ResolvableType.forClass(String.class),
|
||||
BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
}
|
||||
|
||||
static class ConcreteOverrideMetadata extends OverrideMetadata {
|
||||
|
||||
ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride,
|
||||
ConcreteOverrideMetadata(Field field, ResolvableType typeToOverride,
|
||||
BeanOverrideStrategy strategy) {
|
||||
|
||||
super(field, overrideAnnotation, typeToOverride, strategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return ConcreteOverrideMetadata.class.getSimpleName();
|
||||
super(field, typeToOverride, strategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.convention;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@SpringJUnitConfig
|
||||
class AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@TestBean
|
||||
Pojo someBean;
|
||||
|
||||
@TestBean
|
||||
Pojo otherBean;
|
||||
|
||||
@TestBean(name = "thirdBean")
|
||||
Pojo anotherBean;
|
||||
|
||||
static Pojo otherBeanTestOverride() {
|
||||
return new FakePojo("otherBean in superclass");
|
||||
}
|
||||
|
||||
static Pojo thirdBeanTestOverride() {
|
||||
return new FakePojo("third in superclass");
|
||||
}
|
||||
|
||||
static Pojo commonBeanOverride() {
|
||||
return new FakePojo("in superclass");
|
||||
}
|
||||
|
||||
interface Pojo {
|
||||
|
||||
default String getValue() {
|
||||
return "Prod";
|
||||
}
|
||||
}
|
||||
|
||||
static class ProdPojo implements Pojo { }
|
||||
|
||||
static class FakePojo implements Pojo {
|
||||
final String value;
|
||||
|
||||
protected FakePojo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
Pojo someBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo otherBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo thirdBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo2() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
public class TestBeanInheritanceIntegrationTests {
|
||||
|
||||
static AbstractTestBeanIntegrationTestCase.Pojo nestedBeanOverride() {
|
||||
return new AbstractTestBeanIntegrationTestCase.FakePojo("in enclosing test class");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Concrete inherited test with correct @TestBean setup")
|
||||
class ConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@TestBean(methodName = "commonBeanOverride")
|
||||
Pojo pojo;
|
||||
|
||||
@TestBean(methodName = "nestedBeanOverride")
|
||||
Pojo pojo2;
|
||||
|
||||
static Pojo someBeanTestOverride() {
|
||||
return new FakePojo("someBeanOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypeMethodInType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
|
||||
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInTypeMethodInSuperType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
|
||||
assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInTypeMethodInEnclosingClass(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
|
||||
assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypePrioritizeMethodInType(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
|
||||
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void failsIfFieldInSupertypeButNoMethod() {
|
||||
Class<?> clazz = Failing1.class;
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(clazz))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates [someBeanTestOverride]""",
|
||||
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsIfMethod1InSupertypeAndMethod2InType() {
|
||||
Class<?> clazz = Failing2.class;
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(clazz))
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("""
|
||||
Found 2 competing static test bean factory methods in %s with return type %s \
|
||||
whose name matches one of the supported candidates \
|
||||
[thirdBeanTestOverride, anotherBeanTestOverride]""",
|
||||
clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
|
||||
}
|
||||
|
||||
static class Failing1 extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
}
|
||||
}
|
||||
|
||||
static class Failing2 extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
static Pojo someBeanTestOverride() {
|
||||
return new FakePojo("ignored");
|
||||
}
|
||||
|
||||
static Pojo anotherBeanTestOverride() {
|
||||
return new FakePojo("sub2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignored2() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
@SpringJUnitConfig
|
||||
public class TestBeanIntegrationTests {
|
||||
|
||||
@TestBean
|
||||
String field;
|
||||
|
||||
@TestBean
|
||||
String nestedField;
|
||||
|
||||
@TestBean(name = "field")
|
||||
String renamed1;
|
||||
|
||||
@TestBean(name = "nestedField")
|
||||
String renamed2;
|
||||
|
||||
@TestBean(methodName = "fieldTestOverride")
|
||||
String methodRenamed1;
|
||||
|
||||
@TestBean(methodName = "nestedFieldTestOverride")
|
||||
String methodRenamed2;
|
||||
|
||||
static String fieldTestOverride() {
|
||||
return "fieldOverride";
|
||||
}
|
||||
|
||||
static String nestedFieldTestOverride() {
|
||||
return "nestedFieldOverride";
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.field).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
assertThat(this.methodRenamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoFieldNameBean() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing1.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'noOriginalBean'; " +
|
||||
"there is no bean definition to replace with that name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoExplicitNameBean() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing2.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresent'; " +
|
||||
"there is no bean definition to replace with that name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoImplicitMethod() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing3.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Failed to find a static test bean factory method in " +
|
||||
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing3 " +
|
||||
"with return type java.lang.String whose name matches one of the " +
|
||||
"supported candidates [notPresent]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanFailingNoExplicitMethod() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failing4.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.rootCause().isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Failed to find a static test bean factory method in " +
|
||||
"org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing4 " +
|
||||
"with return type java.lang.String whose name matches one of the " +
|
||||
"supported candidates [fieldTestOverride]"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @TestBean on enclosing class")
|
||||
class TestBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.nestedField).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.renamed2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(TestBeanIntegrationTests.this.methodRenamed2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With factory method on enclosing class")
|
||||
class TestBeanNested2 {
|
||||
|
||||
@TestBean(methodName = "nestedFieldTestOverride", name = "nestedField")
|
||||
String nestedField2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
|
||||
assertThat(this.nestedField2).isEqualTo("nestedFieldOverride");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean("field")
|
||||
String bean1() {
|
||||
return "prod";
|
||||
}
|
||||
|
||||
@Bean("nestedField")
|
||||
String bean2() {
|
||||
return "nestedProd";
|
||||
}
|
||||
|
||||
@Bean("methodRenamed1")
|
||||
String bean3() {
|
||||
return "Prod";
|
||||
}
|
||||
|
||||
@Bean("methodRenamed2")
|
||||
String bean4() {
|
||||
return "NestedProd";
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing1 {
|
||||
|
||||
@TestBean
|
||||
String noOriginalBean;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
|
||||
static String noOriginalBeanTestOverride() {
|
||||
return "should be ignored";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing2 {
|
||||
|
||||
@TestBean(name = "notPresent")
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
|
||||
static String notPresentTestOverride() {
|
||||
return "should be ignored";
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing3 {
|
||||
|
||||
@TestBean(methodName = "notPresent")
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failing4 {
|
||||
|
||||
@TestBean //expects fieldTestOverride method
|
||||
String field;
|
||||
|
||||
@Test
|
||||
void ignored() {
|
||||
fail("should fail earlier");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.MethodConventionOverrideMetadata;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.TestBeanOverrideMetadata;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.FailingExampleService;
|
||||
|
||||
@@ -95,7 +96,7 @@ class TestBeanOverrideProcessorTests {
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isThrownBy(() -> processor.createMetadata(overrideAnnotation, clazz, field))
|
||||
.withMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""",
|
||||
@@ -104,35 +105,49 @@ class TestBeanOverrideProcessorTests {
|
||||
|
||||
@Test
|
||||
void createMetaDataForKnownExplicitMethod() throws Exception {
|
||||
Class<?> returnType = ExampleService.class;
|
||||
Field field = ExplicitMethodNameConf.class.getField("b");
|
||||
Class<?> clazz = ExplicitMethodNameConf.class;
|
||||
Field field = clazz.getField("b");
|
||||
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
|
||||
assertThat(overrideAnnotation).isNotNull();
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isInstanceOf(MethodConventionOverrideMetadata.class);
|
||||
assertThat(processor.createMetadata(overrideAnnotation, clazz, field))
|
||||
.isInstanceOf(TestBeanOverrideMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception {
|
||||
void createMetaDataForConventionBasedFactoryMethod() throws Exception {
|
||||
Class<?> returnType = ExampleService.class;
|
||||
Field field = MethodConventionConf.class.getField("field");
|
||||
Class<?> clazz = MethodConventionConf.class;
|
||||
Field field = clazz.getField("field");
|
||||
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
|
||||
assertThat(overrideAnnotation).isNotNull();
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
// When in convention-based mode, createMetadata() will not verify that
|
||||
// the factory method actually exists. So, we don't expect an exception
|
||||
// for this use case.
|
||||
assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
|
||||
.isInstanceOf(MethodConventionOverrideMetadata.class);
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(
|
||||
overrideAnnotation, clazz, field))
|
||||
.withMessage("""
|
||||
Failed to find a static test bean factory method in %s with return type %s \
|
||||
whose name matches one of the supported candidates %s""",
|
||||
clazz.getName(), returnType.getName(), List.of("someFieldTestOverride", "fieldTestOverride"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failToCreateMetadataForOtherAnnotation() throws NoSuchFieldException {
|
||||
Class<?> clazz = MethodConventionConf.class;
|
||||
Field field = clazz.getField("field");
|
||||
NonNull badAnnotation = AnnotationUtils.synthesizeAnnotation(NonNull.class);
|
||||
|
||||
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(badAnnotation, clazz, field))
|
||||
.withMessage("Invalid annotation passed to TestBeanOverrideProcessor: expected @TestBean" +
|
||||
" on field %s.%s", field.getDeclaringClass().getName(), field.getName());
|
||||
}
|
||||
|
||||
|
||||
static class MethodConventionConf {
|
||||
|
||||
@TestBean
|
||||
@TestBean(name = "someField")
|
||||
public ExampleService field;
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -29,6 +29,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
public @interface ExampleBeanOverrideAnnotation {
|
||||
|
||||
String DEFAULT_VALUE = "TEST OVERRIDE";
|
||||
// Any metadata using this as the prefix for the bean name will be considered equal
|
||||
String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
String value() default DEFAULT_VALUE;
|
||||
|
||||
|
||||
@@ -26,24 +26,15 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||
// Intentionally NOT public
|
||||
class ExampleBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
static final String DUPLICATE_TRIGGER = "DUPLICATE";
|
||||
|
||||
private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{DUPLICATE_TRIGGER}";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) {
|
||||
throw new IllegalStateException("unexpected annotation");
|
||||
}
|
||||
if (annotation.value().equals(DUPLICATE_TRIGGER)) {
|
||||
return CONSTANT;
|
||||
if (annotation.value().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER)) {
|
||||
return new TestOverrideMetadata(annotation.value());
|
||||
}
|
||||
return new TestOverrideMetadata(field, annotation, typeToOverride);
|
||||
return new TestOverrideMetadata(field, annotation, ResolvableType.forField(field, testClass));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ class TestOverrideMetadata extends OverrideMetadata {
|
||||
@Nullable
|
||||
private final String beanName;
|
||||
|
||||
private final String methodName;
|
||||
|
||||
@Nullable
|
||||
private static Method findMethod(AnnotatedElement element, String methodName) {
|
||||
if (DEFAULT_VALUE.equals(methodName)) {
|
||||
@@ -77,30 +79,31 @@ class TestOverrideMetadata extends OverrideMetadata {
|
||||
}
|
||||
|
||||
public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) {
|
||||
super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ?
|
||||
super(field, typeToOverride, overrideAnnotation.createIfMissing() ?
|
||||
BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION);
|
||||
this.method = findMethod(field, overrideAnnotation.value());
|
||||
this.methodName = overrideAnnotation.value();
|
||||
this.beanName = overrideAnnotation.beanName();
|
||||
}
|
||||
|
||||
//Used to trigger duplicate detection in parser test
|
||||
TestOverrideMetadata() {
|
||||
super(null, null, null, null);
|
||||
TestOverrideMetadata(String duplicateTrigger) {
|
||||
super(null, null, null);
|
||||
this.method = null;
|
||||
this.beanName = null;
|
||||
this.methodName = duplicateTrigger;
|
||||
this.beanName = duplicateTrigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getExpectedBeanName() {
|
||||
protected String getBeanName() {
|
||||
if (StringUtils.hasText(this.beanName)) {
|
||||
return this.beanName;
|
||||
}
|
||||
return super.getExpectedBeanName();
|
||||
return super.getBeanName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanOverrideDescription() {
|
||||
return "test";
|
||||
String getAnnotationMethodName() {
|
||||
return this.methodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,4 +120,29 @@ class TestOverrideMetadata extends OverrideMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this.method == null) {
|
||||
return obj instanceof TestOverrideMetadata tem &&
|
||||
tem.beanName != null &&
|
||||
tem.beanName.startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (this.method == null) {
|
||||
return ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER.hashCode();
|
||||
}
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (this.method == null) {
|
||||
return "{" + this.beanName + "}";
|
||||
}
|
||||
return this.methodName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringJUnitConfig
|
||||
public class MockitoBeanIntegrationTests {
|
||||
|
||||
@MockitoBean
|
||||
ExampleService field;
|
||||
|
||||
@MockitoBean
|
||||
ExampleService nestedField;
|
||||
|
||||
@MockitoBean(name = "field")
|
||||
ExampleService renamed1;
|
||||
|
||||
@MockitoBean(name = "nestedField")
|
||||
ExampleService renamed2;
|
||||
|
||||
@MockitoBean(name = "nonExistingBean")
|
||||
ExampleService nonExisting1;
|
||||
|
||||
@MockitoBean(name = "nestedNonExistingBean")
|
||||
ExampleService nonExisting2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.field);
|
||||
|
||||
assertThat(this.field.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.renamed1);
|
||||
|
||||
assertThat(this.renamed1.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nonExistingBean"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(this.nonExisting1);
|
||||
|
||||
assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoBean on enclosing class")
|
||||
class MockitoBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.nestedField);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.renamed2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedNonExistingBean"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
|
||||
.as("isMock").isTrue())
|
||||
.isSameAs(MockitoBeanIntegrationTests.this.nonExisting2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Bean("field")
|
||||
ExampleService bean1() {
|
||||
return new RealExampleService("Hello Field");
|
||||
}
|
||||
@Bean("nestedField")
|
||||
ExampleService bean2() {
|
||||
return new RealExampleService("Hello Nested Field");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
public class MockitoBeanOverrideProcessorTests {
|
||||
|
||||
@Test
|
||||
void mockAnnotationCreatesMockMetadata() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
|
||||
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoBeanMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void spyAnnotationCreatesSpyMetadata() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
|
||||
OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanMetadata.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherAnnotationThrows() throws NoSuchFieldException {
|
||||
MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
Annotation annotation = field.getAnnotation(Nullable.class);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(annotation, clazz, field))
|
||||
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected " +
|
||||
"@MockitoBean/@MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
|
||||
field.getName());
|
||||
}
|
||||
|
||||
static class MockitoConf {
|
||||
@Nullable
|
||||
@MockitoBean
|
||||
@MockitoSpyBean
|
||||
public ExampleService a;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.platform.engine.TestExecutionResult;
|
||||
import org.junit.platform.testkit.engine.EngineExecutionResults;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
|
||||
@SpringJUnitConfig(MockitoBeanIntegrationTests.Config.class)
|
||||
public class MockitoSpyBeanIntegrationTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService field;
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService nestedField;
|
||||
|
||||
@MockitoSpyBean(name = "field")
|
||||
ExampleService renamed1;
|
||||
|
||||
@MockitoSpyBean(name = "nestedField")
|
||||
ExampleService renamed2;
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(this.field);
|
||||
|
||||
assertThat(this.field.greeting()).as("spied greeting")
|
||||
.isEqualTo("Hello Field");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(this.renamed1);
|
||||
|
||||
assertThat(this.field.greeting()).as("spied greeting")
|
||||
.isEqualTo("Hello Field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failWhenBeanNotPresentFieldName() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failure1.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresent' by wrapping," +
|
||||
" no existing bean instance by this name of type %s",
|
||||
ExampleService.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failWhenBeanNotPresentExplicitName() {
|
||||
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
|
||||
.selectors(selectClass(Failure2.class))//
|
||||
.execute();
|
||||
|
||||
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
|
||||
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
|
||||
.getThrowable()).get(THROWABLE)
|
||||
.cause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Unable to override bean 'notPresentAtAll' by wrapping," +
|
||||
" no existing bean instance by this name of type %s",
|
||||
ExampleService.class.getName()));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoSpyBean on enclosing class")
|
||||
class MockitoBeanNested {
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(MockitoSpyBeanIntegrationTests.this.nestedField);
|
||||
|
||||
assertThat(MockitoSpyBeanIntegrationTests.this.nestedField.greeting())
|
||||
.as("spied greeting")
|
||||
.isEqualTo("Hello Nested Field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("nestedField"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
|
||||
.as("isSpy").isTrue())
|
||||
.isSameAs(MockitoSpyBeanIntegrationTests.this.renamed2);
|
||||
|
||||
|
||||
assertThat(MockitoSpyBeanIntegrationTests.this.renamed2.greeting())
|
||||
.as("spied greeting")
|
||||
.isEqualTo("Hello Nested Field");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failure1 {
|
||||
|
||||
@MockitoSpyBean
|
||||
ExampleService notPresent;
|
||||
|
||||
@Test
|
||||
void ignored() { }
|
||||
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class Failure2 {
|
||||
|
||||
@MockitoSpyBean(name = "notPresentAtAll")
|
||||
ExampleService field;
|
||||
|
||||
@Test
|
||||
void ignored() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user