Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
package io.microsphere.spring.beans.factory;


import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

Expand All @@ -31,6 +36,8 @@
import java.util.concurrent.ExecutorService;

import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
Expand All @@ -42,6 +49,7 @@
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = DefaultBeanDependencyResolverTest.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultBeanDependencyResolverTest {

private DefaultBeanDependencyResolver resolver;
Expand All @@ -51,20 +59,247 @@ class DefaultBeanDependencyResolverTest {

private ExecutorService executorService;

@BeforeEach
// --- Inner static bean classes used across multiple tests ---

static class ServiceA {
@Autowired
private ServiceB serviceB;
}

static class ServiceB {
}

static class ServiceC {
}

static class PropertyRefBean {
private ServiceB ref;
}

interface ServiceInterface {
}

// --- Lifecycle ---

@BeforeAll
void setUp() {
this.executorService = newSingleThreadExecutor();
this.resolver = new DefaultBeanDependencyResolver(this.beanFactory, this.executorService);
}

@AfterEach
@AfterAll
void tearDown() {
this.executorService.shutdown();
}

// --- Tests ---

/**
* Existing test: resolver uses the same beanFactory that was passed at construction time.
* All beans in the test context are already singletons, so the map is empty.
*/
@Test
void testResolve() {
Map<String, Set<String>> dependentBeanNamesMap = this.resolver.resolve(this.beanFactory);
assertTrue(dependentBeanNamesMap.isEmpty());
}

/**
* When a different (non-matching) beanFactory is passed to resolve(bf), the resolver
* logs a warning and returns an empty map.
*/
@Test
void testResolveWithDifferentBeanFactory() {
DefaultListableBeanFactory anotherFactory = new DefaultListableBeanFactory();
Map<String, Set<String>> result = this.resolver.resolve(anotherFactory);
assertTrue(result.isEmpty());
}

/**
* When a different (non-matching) beanFactory is passed to resolve(name, def, bf), the
* resolver logs a warning and returns an empty set.
*/
@Test
void testResolveByNameWithDifferentBeanFactory() {
DefaultListableBeanFactory anotherFactory = new DefaultListableBeanFactory();
RootBeanDefinition beanDefinition = new RootBeanDefinition(ServiceB.class);
Set<String> result = this.resolver.resolve("serviceB", beanDefinition, anotherFactory);
assertTrue(result.isEmpty());
}

/**
* Direct per-bean resolution via resolve(name, def, factory) for a bean with no dependencies.
*/
@Test
void testResolveByNameDirectNoDeps() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

RootBeanDefinition beanDef = (RootBeanDefinition) factory.getMergedBeanDefinition("serviceB");
Set<String> result = freshResolver.resolve("serviceB", beanDef, factory);
assertTrue(result.isEmpty());
}

/**
* Direct per-bean resolution via resolve(name, def, factory) for a bean that has an
* {@code @Autowired} field.
*/
@Test
void testResolveByNameWithAutowiredField() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition("serviceA", new RootBeanDefinition(ServiceA.class));
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

RootBeanDefinition beanDef = (RootBeanDefinition) factory.getMergedBeanDefinition("serviceA");
Set<String> result = freshResolver.resolve("serviceA", beanDef, factory);
assertTrue(result.contains("serviceB"));
}

/**
* resolve(factory) discovers the {@code @Autowired} field dependency of serviceA on serviceB.
*/
@Test
void testResolveWithAutowiredFieldDependencies() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition("serviceA", new RootBeanDefinition(ServiceA.class));
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
assertFalse(result.isEmpty());
Set<String> serviceADeps = result.get("serviceA");
assertNotNull(serviceADeps);
assertTrue(serviceADeps.contains("serviceB"));
}

/**
* Beans with a {@code depends-on} attribute are resolved as explicit dependencies.
*/
@Test
void testResolveWithDependsOn() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
RootBeanDefinition beanADef = new RootBeanDefinition(ServiceC.class);
beanADef.setDependsOn("serviceB");
factory.registerBeanDefinition("serviceA", beanADef);
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
Set<String> serviceADeps = result.get("serviceA");
assertNotNull(serviceADeps);
assertTrue(serviceADeps.contains("serviceB"));
}

/**
* Beans whose property values contain a {@link RuntimeBeanReference} are recorded as
* depending on the referenced bean.
*/
@Test
void testResolveWithBeanReference() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
RootBeanDefinition beanWithRefDef = new RootBeanDefinition(PropertyRefBean.class);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("ref", new RuntimeBeanReference("serviceB"));
beanWithRefDef.setPropertyValues(pvs);
factory.registerBeanDefinition("propertyRefBean", beanWithRefDef);
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
Set<String> beanDeps = result.get("propertyRefBean");
assertNotNull(beanDeps);
assertTrue(beanDeps.contains("serviceB"));
}

/**
* Beans produced by a factory bean depend on that factory bean.
*/
@Test
void testResolveWithFactoryBeanName() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition("configBean", new RootBeanDefinition(ServiceB.class));
RootBeanDefinition productBeanDef = new RootBeanDefinition(ServiceC.class);
productBeanDef.setFactoryBeanName("configBean");
factory.registerBeanDefinition("productBean", productBeanDef);
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
Set<String> productBeanDeps = result.get("productBean");
assertNotNull(productBeanDeps);
assertTrue(productBeanDeps.contains("configBean"));
}

/**
* Lazy-init beans are excluded from the eligible-bean map and therefore never appear in
* the resolved dependency map.
*/
@Test
void testResolveWithLazyInitBean() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
RootBeanDefinition lazyDef = new RootBeanDefinition(ServiceA.class);
lazyDef.setLazyInit(true);
factory.registerBeanDefinition("lazyBean", lazyDef);
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
assertFalse(result.containsKey("lazyBean"));
}

/**
* Abstract bean definitions are excluded from the eligible-bean map.
*/
@Test
void testResolveWithAbstractBean() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
RootBeanDefinition abstractDef = new RootBeanDefinition(ServiceB.class);
abstractDef.setAbstract(true);
factory.registerBeanDefinition("abstractBean", abstractDef);
factory.registerBeanDefinition("serviceB", new RootBeanDefinition(ServiceB.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
assertFalse(result.containsKey("abstractBean"));
}

/**
* Transitive dependencies are flattened: if A depends on B and B depends on C, the
* resolved map for A includes both B and C.
*/
@Test
void testResolveWithFlattenedDependencies() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
RootBeanDefinition beanADef = new RootBeanDefinition(ServiceC.class);
beanADef.setDependsOn("beanB");
factory.registerBeanDefinition("beanA", beanADef);
RootBeanDefinition beanBDef = new RootBeanDefinition(ServiceC.class);
beanBDef.setDependsOn("beanC");
factory.registerBeanDefinition("beanB", beanBDef);
factory.registerBeanDefinition("beanC", new RootBeanDefinition(ServiceC.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
Set<String> beanADeps = result.get("beanA");
assertNotNull(beanADeps);
assertTrue(beanADeps.contains("beanB"));
assertTrue(beanADeps.contains("beanC")); // transitive dependency via flattening
}

/**
* When the bean class is an interface, the resolver skips injection-point scanning and
* produces an empty dependency set without throwing.
*/
@Test
void testResolveWithInterfaceBeanClass() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition("serviceInterface", new RootBeanDefinition(ServiceInterface.class));
DefaultBeanDependencyResolver freshResolver = new DefaultBeanDependencyResolver(factory, this.executorService);

Map<String, Set<String>> result = freshResolver.resolve(factory);
Set<String> interfaceDeps = result.get("serviceInterface");
assertNotNull(interfaceDeps);
assertTrue(interfaceDeps.isEmpty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.lang.reflect.Constructor;

import static io.microsphere.spring.beans.factory.annotation.AnnotatedInjectionBeanPostProcessorTest.TestConfiguration.Child;
import static io.microsphere.spring.beans.factory.annotation.AnnotatedInjectionBeanPostProcessorTest.TestConfiguration.Parent;
import static io.microsphere.spring.beans.factory.annotation.AnnotatedInjectionBeanPostProcessorTest.TestConfiguration.UserHolder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -110,6 +113,53 @@
});
}

/** Setters persist through getters; verify all configurable flags round-trip. */
@Test
void testSettersAndGetters() {
AnnotatedInjectionBeanPostProcessor p = new AnnotatedInjectionBeanPostProcessor(Referenced.class);
p.setOrder(42);
assertEquals(42, p.getOrder());

p.setClassValuesAsString(true);
p.setNestedAnnotationsAsMap(true);
p.setIgnoreDefaultValue(false);
p.setTryMergedAnnotation(false);
p.setCacheSize(64);
// No exceptions thrown means setters/getters work correctly; verify order
assertEquals(42, p.getOrder());
}

/** afterPropertiesSet initialises caches; destroy clears them without error. */
@Test
void testAfterPropertiesSetAndDestroy() throws Exception {

Check failure on line 134 in microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/annotation/AnnotatedInjectionBeanPostProcessorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-spring&issues=AZ0KaQmcOOAe5IMiMdK8&open=AZ0KaQmcOOAe5IMiMdK8&pullRequest=174

Check warning on line 134 in microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/annotation/AnnotatedInjectionBeanPostProcessorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.lang.Exception', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-spring&issues=AZ0KaQmcOOAe5IMiMdK-&open=AZ0KaQmcOOAe5IMiMdK-&pullRequest=174
AnnotatedInjectionBeanPostProcessor p = new AnnotatedInjectionBeanPostProcessor(Referenced.class);
p.setCacheSize(8);
p.afterPropertiesSet(); // must not throw
p.destroy(); // must not throw
}

/** postProcessMergedBeanDefinition must not throw for a known bean type. */
@Test
void testPostProcessMergedBeanDefinition() {

Check failure on line 143 in microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/annotation/AnnotatedInjectionBeanPostProcessorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-spring&issues=AZ0KaQmcOOAe5IMiMdK9&open=AZ0KaQmcOOAe5IMiMdK9&pullRequest=174
org.springframework.beans.factory.support.RootBeanDefinition rbd =
new org.springframework.beans.factory.support.RootBeanDefinition(TestConfiguration.Parent.class);
// No exception expected
processor.postProcessMergedBeanDefinition(rbd, TestConfiguration.Parent.class, "parent");
}

/** determineCandidateConstructors returns null for a class without injection annotations. */
@Test
void testDetermineCandidateConstructorsNoCandidates() {
// PlainBean has no @Referenced annotation on constructors
Constructor<?>[] result = processor.determineCandidateConstructors(PlainBean.class, "plainBean");
assertNull(result);
}

static class PlainBean {
PlainBean() {}
PlainBean(String s) {}

Check failure on line 160 in microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/annotation/AnnotatedInjectionBeanPostProcessorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-spring&issues=AZ0KaQmcOOAe5IMiMdK_&open=AZ0KaQmcOOAe5IMiMdK_&pullRequest=174
}

<T> T createProxy(Class<T> beanType) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(beanType);
Expand Down
Loading
Loading