diff --git a/boa/src/main/java/beans/PropertyValue.java b/boa/src/main/java/beans/PropertyValue.java index 428dd02..5fd4927 100644 --- a/boa/src/main/java/beans/PropertyValue.java +++ b/boa/src/main/java/beans/PropertyValue.java @@ -37,4 +37,13 @@ public boolean equals(Object obj) { public int hashCode() { return Objects.hash(name, value); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PropertyValue{name='").append(name).append('\''); + sb.append(", value='").append(value).append('\''); + sb.append('}'); + return sb.toString(); + } } diff --git a/boa/src/main/java/beans/PropertyValues.java b/boa/src/main/java/beans/PropertyValues.java index 2ebae71..a6562cf 100644 --- a/boa/src/main/java/beans/PropertyValues.java +++ b/boa/src/main/java/beans/PropertyValues.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * 하나 이상의 PropertyValue 객체를 담는 컨테이너 클래스 입니다. @@ -48,4 +49,26 @@ public PropertyValue getPropertyValue(String propertyName) { public boolean isEmpty() { return this.propertyValueList.isEmpty(); } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + PropertyValues that = (PropertyValues) other; + return Objects.equals(propertyValueList, that.propertyValueList); + } + + @Override + public int hashCode() { + return Objects.hash(propertyValueList); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PropertyValues{"); + sb.append(propertyValueList); + sb.append('}'); + return sb.toString(); + } } diff --git a/boa/src/main/java/beans/factory/config/BeanDefinition.java b/boa/src/main/java/beans/factory/config/BeanDefinition.java index 8fb345c..b3e18a0 100644 --- a/boa/src/main/java/beans/factory/config/BeanDefinition.java +++ b/boa/src/main/java/beans/factory/config/BeanDefinition.java @@ -28,6 +28,9 @@ public BeanDefinition(Class beanClass) { } public BeanDefinition(Class beanClass, PropertyValues propertyValues) { + if (beanClass == null) { + throw new IllegalArgumentException("beanClass cannot be null"); + } this.beanClass = beanClass; this.propertyValues = propertyValues != null ? propertyValues : new PropertyValues(); } @@ -112,6 +115,7 @@ public String toString() { sb.append(getBeanClassName()).append("]"); sb.append("; scope=").append(scope); sb.append("; lazyInit=").append(lazyInit); + sb.append("; propertyValues=").append(propertyValues); sb.append("; initMethodName=").append(initMethodName); sb.append("; destroyMethodName=").append(destroyMethodName); return sb.toString(); diff --git a/boa/src/main/java/beans/factory/support/DefaultListableBeanFactory.java b/boa/src/main/java/beans/factory/support/DefaultListableBeanFactory.java index 94b2876..481a238 100644 --- a/boa/src/main/java/beans/factory/support/DefaultListableBeanFactory.java +++ b/boa/src/main/java/beans/factory/support/DefaultListableBeanFactory.java @@ -18,6 +18,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) { + if (beanName == null) { + throw new IllegalArgumentException("Bean name must not be null"); + } + if (beanName.isEmpty()) { + throw new IllegalArgumentException("Bean name must not be empty"); + } + if (beanDefinition == null) { + throw new IllegalArgumentException("BeanDefinition must not be null"); + } beanDefinitionMap.put(beanName, beanDefinition); } diff --git a/boa/src/test/java/bean/BeanFactoryTest.java b/boa/src/test/java/bean/BeanFactoryTest.java deleted file mode 100644 index 78aebf5..0000000 --- a/boa/src/test/java/bean/BeanFactoryTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package bean; - -import beans.PropertyValue; -import beans.PropertyValues; -import beans.factory.config.BeanDefinition; -import beans.factory.config.BeanReference; -import beans.factory.support.DefaultListableBeanFactory; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class BeanFactoryTest { - @Test - @DisplayName("가장 기본적인 BeanFactory 테스트: 빈 정의 등록 및 조회") - void testBasicBeanFactory() { - // given - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); - beanFactory.registerBeanDefinition("userRepository", beanDefinition); - - // when - UserRepository userRepository = (UserRepository) beanFactory.getBean("userRepository"); - - // then - assertThat(userRepository).isNotNull(); - assertThat(userRepository.findById("1")).isEqualTo("User1"); - } - - @Test - @DisplayName("싱글톤 빈은 항상 동일한 인스턴스를 반환해야 한다") - void testSingletonBean() { - // given - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); - beanFactory.registerBeanDefinition("userRepository", beanDefinition); - - // when - Object bean1 = beanFactory.getBean("userRepository"); - Object bean2 = beanFactory.getBean("userRepository"); - - // then - assertThat(bean1).isNotNull(); - assertThat(bean2).isNotNull(); - assertThat(bean1).isSameAs(bean2); - } - - @Test - @DisplayName("의존성 주입 테스트: 빈의 프로퍼티에 다른 빈을 주입할 수 있다") - void testDependencyInjection() { - // given - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - // UserRepository 빈 정의 등록 - BeanDefinition userRepositoryDefinition = new BeanDefinition(UserRepository.class); - beanFactory.registerBeanDefinition("userRepository", userRepositoryDefinition); - - // userService 빈 정의 등록 - PropertyValues propertyValues = new PropertyValues(); - propertyValues.addPropertyValue(new PropertyValue("userRepository", new BeanReference("userRepository"))); - BeanDefinition userServiceDefinition = new BeanDefinition(UserService.class, propertyValues); - beanFactory.registerBeanDefinition("userService", userServiceDefinition); - - // when - UserService userService = (UserService) beanFactory.getBean("userService"); - - // then - assertThat(userService).isNotNull(); - assertThat(userService.getUserRepository()).isNotNull(); - assertThat(userService.findUser("1")).isEqualTo("User1"); - } -} diff --git a/boa/src/test/java/beans/factory/BeanLifecycleIntegrationTest.java b/boa/src/test/java/beans/factory/BeanLifecycleIntegrationTest.java new file mode 100644 index 0000000..0cc54fb --- /dev/null +++ b/boa/src/test/java/beans/factory/BeanLifecycleIntegrationTest.java @@ -0,0 +1,294 @@ +package beans.factory; + +import beans.BeansException; +import beans.PropertyValue; +import beans.PropertyValues; +import beans.factory.config.BeanDefinition; +import beans.factory.config.BeanReference; +import beans.factory.support.DefaultListableBeanFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class BeanLifecycleIntegrationTest { + + private DefaultListableBeanFactory beanFactory; + + @BeforeEach + void setUp() { + beanFactory = new DefaultListableBeanFactory(); + } + + @Test + @DisplayName("InitializingBean 구현체의 afterPropertiesSet이 호출되어야 한다") + void testInitializingBeanCallback() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestInitializingBean.class); + beanFactory.registerBeanDefinition("initializingBean", beanDefinition); + + // when + TestInitializingBean bean = (TestInitializingBean) beanFactory.getBean("initializingBean"); + + // then + assertThat(bean).isNotNull(); + assertThat(bean.isInitialized()).isTrue(); // afterPropertiesSet 메서드가 호출되면 true됨 + assertThat(bean.getInitializationOrder()).contains("afterPropertiesSet"); + } + + // TODO: 추후에 @PostConstruct -> InitializingBean -> 커스텀 초기화 메서드 순으로 호출되는지 확인하는 테스트 추가 + @Test + @DisplayName("InitializingBean과 커스텀 초기화 메서드가 모두 있을 때 올바른 순서로 호출되어야 한다") + void testInitializingBeanWithCustomInitMethod() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestInitializingBeanWithCustomInit.class); + beanDefinition.setInitMethodName("customInit"); + beanFactory.registerBeanDefinition("bean", beanDefinition); + + // when + TestInitializingBeanWithCustomInit bean = (TestInitializingBeanWithCustomInit) beanFactory.getBean("bean"); + + // then + assertThat(bean).isNotNull(); + assertThat(bean.getInitializationOrder()).isEqualTo("afterPropertiesSet,customInit"); + } + + @Test + @DisplayName("BeanFactoryAware 구현체에 BeanFactory가 주입되어야 한다") + void testBeanFactoryAwareInjection() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestBeanFactoryAware.class); + beanFactory.registerBeanDefinition("beanFactoryAware", beanDefinition); + + // when + TestBeanFactoryAware bean = (TestBeanFactoryAware) beanFactory.getBean("beanFactoryAware"); + + // then + assertThat(bean).isNotNull(); + assertThat(bean.getBeanFactory()).isNotNull(); + assertThat(bean.getBeanFactory()).isSameAs(beanFactory); + } + + // === 복합 생명주기 테스트 === + @Test + @DisplayName("BeanFactoryAware와 InitializingBean을 모두 구현한 빈의 생명주기가 올바르게 동작해야 한다") + void testCombinedLifecycleInterfaces() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestCombinedLifecycleBean.class); + beanFactory.registerBeanDefinition("combinedBean", beanDefinition); + + // when + TestCombinedLifecycleBean bean = (TestCombinedLifecycleBean) beanFactory.getBean("combinedBean"); + + // then + assertThat(bean).isNotNull(); + assertThat(bean.getBeanFactory()).isSameAs(beanFactory); + assertThat(bean.isInitialized()).isTrue(); + assertThat(bean.getLifecycleOrder()).isEqualTo("setBeanFactory,afterPropertiesSet"); + } + + // 의존성 주입 -> setBeanFactory -> afterPropertiesSet 순서로 실행되는지 확인 + @Test + @DisplayName("의존성 주입과 생명주기 콜백이 올바른 순서로 실행되어야 한다") + void testDependencyInjectionWithLifecycle() { + // given + // 의존성 빈 등록 + BeanDefinition dependencyDefinition = new BeanDefinition(SimpleDependency.class); + beanFactory.registerBeanDefinition("dependency", dependencyDefinition); + + // 주 빈 등록 (의존성 + 생명주기) + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("dependency", new BeanReference("dependency"))); + BeanDefinition mainDefinition = new BeanDefinition(TestBeanWithDependencyAndLifecycle.class, propertyValues); + beanFactory.registerBeanDefinition("mainBean", mainDefinition); + + // when + TestBeanWithDependencyAndLifecycle bean = (TestBeanWithDependencyAndLifecycle) beanFactory.getBean("mainBean"); + + // then + assertThat(bean).isNotNull(); + assertThat(bean.getDependency()).isNotNull(); + assertThat(bean.getBeanFactory()).isSameAs(beanFactory); + assertThat(bean.isInitialized()).isTrue(); + + // 의존성 주입 후 생명주기 콜백이 실행되어야 함 + assertThat(bean.getLifecycleOrder()).containsExactly("setBeanFactory", "afterPropertiesSet"); + assertThat(bean.isDependencyAvailableInInit()).isTrue(); + } + + // === 예외 상황 테스트 === + @Test + @DisplayName("InitializingBean의 afterPropertiesSet에서 예외 발생 시 빈 생성이 실패해야 한다") + void testInitializingBeanThrowsException() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestInitializingBeanWithException.class); + beanFactory.registerBeanDefinition("exceptionBean", beanDefinition); + + // when & then + assertThatThrownBy(() -> beanFactory.getBean("exceptionBean")) + .isInstanceOf(BeansException.class); + } + + // === 테스트용 클래스들 === + + /** + * InitializingBean 구현 테스트 클래스 + */ + public static class TestInitializingBean implements InitializingBean { + private boolean initialized = false; + private String initializationOrder = ""; + + @Override + public void afterPropertiesSet() throws Exception { + this.initialized = true; + this.initializationOrder += "afterPropertiesSet"; + } + + public boolean isInitialized() { + return initialized; + } + + public String getInitializationOrder() { + return initializationOrder; + } + } + + /** + * InitializingBean + 커스텀 초기화 메서드 구현 테스트 클래스 + */ + public static class TestInitializingBeanWithCustomInit implements InitializingBean { + private StringBuilder initializationOrder = new StringBuilder(); + + @Override + public void afterPropertiesSet() throws Exception { + initializationOrder.append("afterPropertiesSet,"); + } + + public void customInit() { + initializationOrder.append("customInit"); + } + + public String getInitializationOrder() { + return initializationOrder.toString(); + } + } + + /** + * BeanFactoryAware 구현 테스트 클래스 + */ + public static class TestBeanFactoryAware implements BeanFactoryAware { + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + } + + /** + * BeanFactoryAware + InitializingBean 복합 구현 테스트 클래스 + */ + public static class TestCombinedLifecycleBean implements BeanFactoryAware, InitializingBean { + private BeanFactory beanFactory; + private boolean initialized = false; + private StringBuilder lifecycleOrder = new StringBuilder(); + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + lifecycleOrder.append("setBeanFactory,"); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.initialized = true; + lifecycleOrder.append("afterPropertiesSet"); + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public boolean isInitialized() { + return initialized; + } + + public String getLifecycleOrder() { + return lifecycleOrder.toString(); + } + } + + /** + * 의존성 + 생명주기 복합 테스트 클래스 + */ + public static class TestBeanWithDependencyAndLifecycle implements BeanFactoryAware, InitializingBean { + private SimpleDependency dependency; + private BeanFactory beanFactory; + private boolean initialized = false; + private boolean dependencyAvailableInInit = false; + private StringBuilder lifecycleOrder = new StringBuilder(); + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + lifecycleOrder.append("setBeanFactory,"); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.initialized = true; + this.dependencyAvailableInInit = (this.dependency != null); + lifecycleOrder.append("afterPropertiesSet,"); + } + + public SimpleDependency getDependency() { + return dependency; + } + + public void setDependency(SimpleDependency dependency) { + this.dependency = dependency; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public boolean isInitialized() { + return initialized; + } + + public boolean isDependencyAvailableInInit() { + return dependencyAvailableInInit; + } + + public String[] getLifecycleOrder() { + return lifecycleOrder.toString().split(","); + } + } + + /** + * 단순한 의존성 클래스 + */ + public static class SimpleDependency { + private String value = "dependency"; + + public String getValue() { + return value; + } + } + + /** + * InitializingBean에서 예외를 발생시키는 테스트 클래스 + */ + public static class TestInitializingBeanWithException implements InitializingBean { + @Override + public void afterPropertiesSet() throws Exception { + throw new RuntimeException("Initialization failed"); + } + } +} diff --git a/boa/src/test/java/beans/factory/PropertyValueTest.java b/boa/src/test/java/beans/factory/PropertyValueTest.java new file mode 100644 index 0000000..60b36f2 --- /dev/null +++ b/boa/src/test/java/beans/factory/PropertyValueTest.java @@ -0,0 +1,196 @@ +package beans.factory; + +import beans.PropertyValue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class PropertyValueTest { + + @Test + @DisplayName("PropertyValue 생성자 테스트: 이름과 값이 정상적으로 저장된다") + void propertyValueConstructor() { + // given + String name = "username"; + String value = "john"; + + // when + PropertyValue propertyValue = new PropertyValue(name, value); + + // then + assertThat(propertyValue.getName()).isEqualTo(name); + assertThat(propertyValue.getValue()).isEqualTo(value); + } + + @Test + @DisplayName("PropertyValue 생성자 테스트: null 값도 정상적으로 저장된다") + void propertyValueConstructor_WithNullValue() { + // given + String name = "description"; + Object value = null; + + // when + PropertyValue propertyValue = new PropertyValue(name, value); + + // then + assertThat(propertyValue.getName()).isEqualTo(name); + assertThat(propertyValue.getValue()).isNull(); + } + + @Test + @DisplayName("PropertyValue 생성자 테스트: 다양한 타입의 값을 저장할 수 있다") + void propertyValueConstructor_WithDifferentTypes() { + // given & when + PropertyValue stringProperty = new PropertyValue("name", "test"); + PropertyValue intProperty = new PropertyValue("age", 25); + PropertyValue booleanProperty = new PropertyValue("active", true); + PropertyValue doubleProperty = new PropertyValue("price", 19.99); + + // then + assertThat(stringProperty.getValue()).isEqualTo("test"); + assertThat(intProperty.getValue()).isEqualTo(25); + assertThat(booleanProperty.getValue()).isEqualTo(true); + assertThat(doubleProperty.getValue()).isEqualTo(19.99); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 같은 이름과 값을 가진 경우") + void propertyValueEquals_SameNameAndValue() { + // given + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("username", "john"); + + // when & then + assertThat(property1).isEqualTo(property2); + assertThat(property1.hashCode()).hasSameHashCodeAs(property2.hashCode()); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 다른 이름을 가진 경우") + void propertyValueEquals_DifferentName() { + // given + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("email", "john"); + + // when & then + assertThat(property1).isNotEqualTo(property2); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 다른 값을 가진 경우") + void propertyValueEquals_DifferentValue() { + // given + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("username", "jane"); + + // when & then + assertThat(property1).isNotEqualTo(property2); + } + + @Test + @DisplayName("PropertyValue equals 테스트: null 값을 가진 경우") + void propertyValueEquals_WithNullValues() { + // given + PropertyValue property1 = new PropertyValue("description", null); + PropertyValue property2 = new PropertyValue("description", null); + + // when & then + assertThat(property1).isEqualTo(property2); + assertThat(property1.hashCode()).hasSameHashCodeAs(property2.hashCode()); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 하나만 null 값을 가진 경우") + void propertyValueEquals_OneNullValue() { + // given + PropertyValue property1 = new PropertyValue("description", "test"); + PropertyValue property2 = new PropertyValue("description", null); + + // when & then + assertThat(property1).isNotEqualTo(property2); + } + + @Test + @DisplayName("PropertyValue equals 테스트: null 객체와 비교") + void propertyValueEquals_WithNull() { + // given + PropertyValue property = new PropertyValue("username", "john"); + + // when & then + assertThat(property).isNotEqualTo(null); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 자기 자신과 비교") + void propertyValueEquals_WithSelf() { + // given + PropertyValue property = new PropertyValue("username", "john"); + + // when & then + assertThat(property).isEqualTo(property); + } + + @Test + @DisplayName("PropertyValue equals 테스트: 다른 타입 객체와 비교") + void propertyValueEquals_WithDifferentType() { + // given + PropertyValue property = new PropertyValue("username", "john"); + String otherObject = "some string"; + + // when & then + assertThat(property).isNotEqualTo(otherObject); + } + + @Test + @DisplayName("PropertyValue toString 테스트: 정상적인 형식으로 출력된다") + void propertyValueToString() { + // given + PropertyValue property = new PropertyValue("username", "john"); + + // when + String result = property.toString(); + + // then + assertThat(result).contains("PropertyValue") + .contains("name='username'") + .contains("value='john'"); + } + + @Test + @DisplayName("PropertyValue toString 테스트: null 값도 정상적으로 출력된다") + void propertyValueToString_WithNullValue() { + // given + PropertyValue property = new PropertyValue("description", null); + + // when + String result = property.toString(); + + // then + assertThat(result).contains("PropertyValue") + .contains("name='description'") + .contains("value='null'"); + } + + @Test + @DisplayName("PropertyValue hashCode 테스트: 같은 내용이면 같은 해시코드") + void propertyValueHashCode_SameContent() { + // given + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("username", "john"); + + // when & then + assertThat(property1.hashCode()).hasSameHashCodeAs(property2.hashCode()); + } + + @Test + @DisplayName("PropertyValue hashCode 테스트: 다른 내용이면 다른 해시코드") + void propertyValueHashCode_DifferentContent() { + // given + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("username", "jane"); + + // when & then + assertThat(property1.hashCode()).isNotEqualTo(property2.hashCode()); + } +} diff --git a/boa/src/test/java/beans/factory/PropertyValuesTest.java b/boa/src/test/java/beans/factory/PropertyValuesTest.java new file mode 100644 index 0000000..5ddf705 --- /dev/null +++ b/boa/src/test/java/beans/factory/PropertyValuesTest.java @@ -0,0 +1,299 @@ +package beans.factory; + +import beans.PropertyValue; +import beans.PropertyValues; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertyValuesTest { + + @Test + @DisplayName("PropertyValues 생성자 테스트: 빈 컬렉션으로 초기화된다") + void propertyValuesConstructor() { + // when + PropertyValues propertyValues = new PropertyValues(); + + // then + assertThat(propertyValues.getPropertyValues()).isNotNull(); + assertThat(propertyValues.getPropertyValues()).isEmpty(); + assertThat(propertyValues.isEmpty()).isTrue(); + } + + @Test + @DisplayName("PropertyValues addPropertyValue 테스트: PropertyValue를 추가할 수 있다") + void addPropertyValue() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue propertyValue = new PropertyValue("username", "john"); + + // when + propertyValues.addPropertyValue(propertyValue); + + // then + assertThat(propertyValues.getPropertyValues()).hasSize(1); + assertThat(propertyValues.getPropertyValues()).contains(propertyValue); + assertThat(propertyValues.isEmpty()).isFalse(); + } + + @Test + @DisplayName("PropertyValues addPropertyValue 테스트: 여러 PropertyValue를 추가할 수 있다") + void addMultiplePropertyValues() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("age", 25); + PropertyValue property3 = new PropertyValue("email", "john@example.com"); + + // when + propertyValues.addPropertyValue(property1); + propertyValues.addPropertyValue(property2); + propertyValues.addPropertyValue(property3); + + // then + assertThat(propertyValues.getPropertyValues()).hasSize(3); + assertThat(propertyValues.getPropertyValues()).containsExactly(property1, property2, property3); + assertThat(propertyValues.isEmpty()).isFalse(); + } + + @Test + @DisplayName("PropertyValues getPropertyValue 테스트: 존재하는 이름으로 조회 성공") + void getPropertyValue_ExistingName() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue usernameProperty = new PropertyValue("username", "john"); + PropertyValue ageProperty = new PropertyValue("age", 25); + propertyValues.addPropertyValue(usernameProperty); + propertyValues.addPropertyValue(ageProperty); + + // when + PropertyValue result = propertyValues.getPropertyValue("username"); + + // then + assertThat(result).isEqualTo(usernameProperty); + assertThat(result.getName()).isEqualTo("username"); + assertThat(result.getValue()).isEqualTo("john"); + } + + @Test + @DisplayName("PropertyValues getPropertyValue 테스트: 존재하지 않는 이름으로 조회 시 null 반환") + void getPropertyValue_NonExistingName() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue property = new PropertyValue("username", "john"); + propertyValues.addPropertyValue(property); + + // when + PropertyValue result = propertyValues.getPropertyValue("email"); + + // then + assertThat(result).isNull(); + } + + @Test + @DisplayName("PropertyValues getPropertyValue 테스트: 빈 컬렉션에서 조회 시 null 반환") + void getPropertyValue_EmptyCollection() { + // given + PropertyValues propertyValues = new PropertyValues(); + + // when + PropertyValue result = propertyValues.getPropertyValue("username"); + + // then + assertThat(result).isNull(); + } + + @Test + @DisplayName("PropertyValues isEmpty 테스트: 빈 상태 확인") + void isEmpty_EmptyState() { + // given + PropertyValues propertyValues = new PropertyValues(); + + // when & then + assertThat(propertyValues.isEmpty()).isTrue(); + assertThat(propertyValues.getPropertyValues()).isEmpty(); + } + + @Test + @DisplayName("PropertyValues isEmpty 테스트: 요소가 있는 상태 확인") + void isEmpty_WithElements() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue property = new PropertyValue("username", "john"); + propertyValues.addPropertyValue(property); + + // when & then + assertThat(propertyValues.isEmpty()).isFalse(); + assertThat(propertyValues.getPropertyValues()).isNotEmpty(); + } + + @Test + @DisplayName("PropertyValues 동일한 이름의 PropertyValue 중복 추가 테스트") + void addPropertyValue_DuplicateName() { + // given + PropertyValues propertyValues = new PropertyValues(); + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("username", "jane"); + + // when + propertyValues.addPropertyValue(property1); + propertyValues.addPropertyValue(property2); + + // then + assertThat(propertyValues.getPropertyValues()).hasSize(2); + assertThat(propertyValues.getPropertyValues()).containsExactly(property1, property2); + + // getPropertyValue는 첫 번째로 찾은 것을 반환 + PropertyValue found = propertyValues.getPropertyValue("username"); + assertThat(found).isEqualTo(property1); + assertThat(found.getValue()).isEqualTo("john"); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 같은 PropertyValue들을 가진 경우") + void propertyValuesEquals_SameProperties() { + // given + PropertyValues propertyValues1 = new PropertyValues(); + PropertyValues propertyValues2 = new PropertyValues(); + + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("age", 25); + + propertyValues1.addPropertyValue(property1); + propertyValues1.addPropertyValue(property2); + + propertyValues2.addPropertyValue(new PropertyValue("username", "john")); + propertyValues2.addPropertyValue(new PropertyValue("age", 25)); + + // when & then + assertThat(propertyValues1).isEqualTo(propertyValues2); + assertThat(propertyValues1.hashCode()).hasSameHashCodeAs(propertyValues2.hashCode()); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 다른 PropertyValue들을 가진 경우") + void propertyValuesEquals_DifferentProperties() { + // given + PropertyValues propertyValues1 = new PropertyValues(); + PropertyValues propertyValues2 = new PropertyValues(); + + propertyValues1.addPropertyValue(new PropertyValue("username", "john")); + propertyValues2.addPropertyValue(new PropertyValue("username", "jane")); + + // when & then + assertThat(propertyValues1).isNotEqualTo(propertyValues2); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 빈 PropertyValues끼리 비교") + void propertyValuesEquals_EmptyCollections() { + // given + PropertyValues propertyValues1 = new PropertyValues(); + PropertyValues propertyValues2 = new PropertyValues(); + + // when & then + assertThat(propertyValues1).isEqualTo(propertyValues2); + assertThat(propertyValues1.hashCode()).hasSameHashCodeAs(propertyValues2.hashCode()); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 자기 자신과 비교") + void propertyValuesEquals_WithSelf() { + // given + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("username", "john")); + + // when & then + assertThat(propertyValues).isEqualTo(propertyValues); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 다른 타입 객체와 비교") + void propertyValuesEquals_WithDifferentType() { + // given + PropertyValues propertyValues = new PropertyValues(); + String otherObject = "some string"; + + // when & then + assertThat(propertyValues).isNotEqualTo(otherObject); + } + + @Test + @DisplayName("PropertyValues equals 테스트: 순서가 다른 경우") + void propertyValuesEquals_DifferentOrder() { + // given + PropertyValues propertyValues1 = new PropertyValues(); + PropertyValues propertyValues2 = new PropertyValues(); + + PropertyValue property1 = new PropertyValue("username", "john"); + PropertyValue property2 = new PropertyValue("age", 25); + + // 다른 순서로 추가 + propertyValues1.addPropertyValue(property1); + propertyValues1.addPropertyValue(property2); + + propertyValues2.addPropertyValue(new PropertyValue("age", 25)); + propertyValues2.addPropertyValue(new PropertyValue("username", "john")); + + // when & then + // ArrayList의 equals는 순서를 고려하므로 다를 것 + assertThat(propertyValues1).isNotEqualTo(propertyValues2); + } + + @Test + @DisplayName("PropertyValues toString 테스트: 정상적인 형식으로 출력된다") + void propertyValuesToString() { + // given + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("username", "john")); + propertyValues.addPropertyValue(new PropertyValue("age", 25)); + + // when + String result = propertyValues.toString(); + + // then + assertThat(result).contains("PropertyValue") + .contains("username") + .contains("john") + .contains("age") + .contains("25"); + } + + @Test + @DisplayName("PropertyValues toString 테스트: 빈 컬렉션도 정상적으로 출력된다") + void propertyValuesToString_EmptyCollection() { + // given + PropertyValues propertyValues = new PropertyValues(); + + // when + String result = propertyValues.toString(); + + // then + assertThat(result).contains("[]"); + } + + @Test + @DisplayName("PropertyValues 통합 테스트: 복합적인 시나리오") + void propertyValuesIntegrationTest() { + // given + PropertyValues propertyValues = new PropertyValues(); + + // when + propertyValues.addPropertyValue(new PropertyValue("name", "TestService")); + propertyValues.addPropertyValue(new PropertyValue("version", "1.0")); + propertyValues.addPropertyValue(new PropertyValue("timeout", 5000)); + propertyValues.addPropertyValue(new PropertyValue("active", true)); + + // then + assertThat(propertyValues.isEmpty()).isFalse(); + assertThat(propertyValues.getPropertyValues()).hasSize(4); + + assertThat(propertyValues.getPropertyValue("name").getValue()).isEqualTo("TestService"); + assertThat(propertyValues.getPropertyValue("version").getValue()).isEqualTo("1.0"); + assertThat(propertyValues.getPropertyValue("timeout").getValue()).isEqualTo(5000); + assertThat(propertyValues.getPropertyValue("active").getValue()).isEqualTo(true); + + assertThat(propertyValues.getPropertyValue("nonexistent")).isNull(); + } +} diff --git a/boa/src/test/java/bean/UserRepository.java b/boa/src/test/java/beans/factory/UserRepository.java similarity index 82% rename from boa/src/test/java/bean/UserRepository.java rename to boa/src/test/java/beans/factory/UserRepository.java index c0ffc42..7ca71f8 100644 --- a/boa/src/test/java/bean/UserRepository.java +++ b/boa/src/test/java/beans/factory/UserRepository.java @@ -1,4 +1,4 @@ -package bean; +package beans.factory; public class UserRepository { public String findById(String id) { diff --git a/boa/src/test/java/bean/UserService.java b/boa/src/test/java/beans/factory/UserService.java similarity index 96% rename from boa/src/test/java/bean/UserService.java rename to boa/src/test/java/beans/factory/UserService.java index 93674ed..916e3a0 100644 --- a/boa/src/test/java/bean/UserService.java +++ b/boa/src/test/java/beans/factory/UserService.java @@ -1,4 +1,4 @@ -package bean; +package beans.factory; public class UserService { private String name; diff --git a/boa/src/test/java/beans/factory/config/BeanDefinitionTest.java b/boa/src/test/java/beans/factory/config/BeanDefinitionTest.java new file mode 100644 index 0000000..d718867 --- /dev/null +++ b/boa/src/test/java/beans/factory/config/BeanDefinitionTest.java @@ -0,0 +1,294 @@ +package beans.factory.config; + +import beans.factory.UserRepository; +import beans.factory.UserService; +import beans.PropertyValue; +import beans.PropertyValues; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class BeanDefinitionTest { + + @Test + @DisplayName("BeanDefinition 생성자 테스트: 클래스만 전달") + void beanDefinitionClassIsNotNull() { + // given + Class beanClass = UserService.class; + + // when + BeanDefinition beanDefinition = new BeanDefinition(beanClass); + + // then + assertThat(beanDefinition.getBeanClass()).isEqualTo(beanClass); + } + + @Test + @DisplayName("BeanDefinition 생성자 테스트: 클래스와 PropertyValues null 전달") + void propertyValuesIsNull() { + // given + Class beanClass = UserService.class; + + // when + BeanDefinition beanDefinition = new BeanDefinition(beanClass, null); + + // then + assertThat(beanDefinition.getPropertyValues()).isNotNull(); + assertThat(beanDefinition.getPropertyValues().getPropertyValues()).isEmpty(); + } + + @Test + @DisplayName("BeanDefinition 생성자 테스트: 클래스와 PropertyValues 전달") + void setPropertyValuesIsNotNull() { + // given + Class beanClass = UserService.class; + PropertyValues propertyValues = new PropertyValues(); + PropertyValue propertyValue = new PropertyValue("name", "testName"); + propertyValues.addPropertyValue(propertyValue); + + // when + BeanDefinition beanDefinition = new BeanDefinition(beanClass, propertyValues); + + // then + assertThat(beanDefinition.getPropertyValues()).isSameAs(propertyValues); + assertThat(beanDefinition.getPropertyValues().getPropertyValue("name")).isEqualTo(propertyValue); + } + + @Test + @DisplayName("BeanDefinition 기본 스코프는 싱글톤이다") + void beanDefinitionDefaultScope() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + + // when & then + assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_SINGLETON); + assertThat(beanDefinition.isSingleton()).isTrue(); + assertThat(beanDefinition.isPrototype()).isFalse(); + } + + @Test + @DisplayName("BeanDefinition 스코프를 프로토타입으로 설정하고 확인한다") + void beanDefinitionSetPrototypeScope() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + + // when + beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + + // then + assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE); + assertThat(beanDefinition.isPrototype()).isTrue(); + assertThat(beanDefinition.isSingleton()).isFalse(); + } + + @Test + @DisplayName("BeanDefinition의 init 메서드 이름을 설정하고 조회한다") + void beanDefinitionSetInitMethodName() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + String initMethodName = "initialize"; + + // when + beanDefinition.setInitMethodName(initMethodName); + + // then + assertThat(beanDefinition.getInitMethodName()).isEqualTo(initMethodName); + } + + @Test + @DisplayName("BeanDefinition의 destroy 메서드 이름을 설정하고 조회한다") + void beanDefinitionSetDestroyMethodName() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + String destroyMethodName = "cleanup"; + + // when + beanDefinition.setDestroyMethodName(destroyMethodName); + + // then + assertThat(beanDefinition.getDestroyMethodName()).isEqualTo(destroyMethodName); + } + + @Test + @DisplayName("BeanDefinition의 기본 지연 초기화 설정은 false이다") + void beanDefinitionDefaultLazyInit() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + + // when & then + assertThat(beanDefinition.isLazyInit()).isFalse(); + } + + @Test + @DisplayName("BeanDefinition의 지연 초기화 설정을 true로 하고 확인한다") + void beanDefinitionSetLazyInit() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + + // when + beanDefinition.setLazyInit(true); + + // then + assertThat(beanDefinition.isLazyInit()).isTrue(); + } + + @Test + @DisplayName("BeanDefinition의 equals와 hashCode 메서드 테스트 - 동일한 클래스와 PropertyValues를 가진 경우") + void beanDefinitionEqualsAndHashCode() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + BeanDefinition definition2 = new BeanDefinition(UserService.class); + + // when & then + assertThat(definition1).isEqualTo(definition2); + assertThat(definition1.hashCode()).hasSameHashCodeAs(definition2.hashCode()); + } + + @Test + @DisplayName("BeanDefinition의 equals와 hashCode 메서드 테스트 - 다른 클래스인 경우") + void beanDefinitionEqualsAndHashCode_DifferentClasses() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + BeanDefinition definition2 = new BeanDefinition(UserRepository.class); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + assertThat(definition1.hashCode()).isNotEqualTo(definition2.hashCode()); + } + + @Test + @DisplayName("BeanDefinition의 equals와 hashCode 메서드 테스트 - 동일한 클래스지만 PropertyValues가 다른 경우") + void beanDefinitionEqualsAndHashCode_DifferentPropertyValues() { + // given + PropertyValues pv1 = new PropertyValues(); + pv1.addPropertyValue(new PropertyValue("name", "service1")); + + PropertyValues pv2 = new PropertyValues(); + pv2.addPropertyValue(new PropertyValue("name", "service2")); + + BeanDefinition definition1 = new BeanDefinition(UserService.class, pv1); + BeanDefinition definition2 = new BeanDefinition(UserService.class, pv2); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + assertThat(definition1.hashCode()).isNotEqualTo(definition2.hashCode()); + } + + @Test + @DisplayName("BeanDefinition의 equals와 hashCode 메서드 테스트 - initMethodName과 destroyMethodName이 있는 경우") + void beanDefinitionEqualsAndHashCode_initMethodNameAndDestroyMethodName() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + definition1.setInitMethodName("init"); + definition1.setDestroyMethodName("destroy"); + BeanDefinition definition2 = new BeanDefinition(UserService.class); + + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + assertThat(definition1.hashCode()).isNotEqualTo(definition2.hashCode()); + } + + + @Test + @DisplayName("BeanDefinition의 toString 메서드 테스트") + void beanDefinitionToString() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserService.class); + beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanDefinition.setInitMethodName("init"); + + // when + String result = beanDefinition.toString(); + + // then + assertThat(result).contains("UserService") + .contains("prototype").contains("init"); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - 자기 자신과의 비교") + void beanDefinitionEquals_WithSelf() { + // given + BeanDefinition definition = new BeanDefinition(UserService.class); + + // when & then + assertThat(definition).isEqualTo(definition); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - 다른 타입 객체와의 비교") + void beanDefinitionEquals_WithDifferentType() { + // given + BeanDefinition definition = new BeanDefinition(UserService.class); + String otherObject = "some string"; + + // when & then + assertThat(definition).isNotEqualTo(otherObject); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - 스코프가 다른 경우") + void beanDefinitionEquals_DifferentScope() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + definition1.setScope(BeanDefinition.SCOPE_SINGLETON); + + BeanDefinition definition2 = new BeanDefinition(UserService.class); + definition2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - lazyInit이 다른 경우") + void beanDefinitionEquals_DifferentLazyInit() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + definition1.setLazyInit(false); + + BeanDefinition definition2 = new BeanDefinition(UserService.class); + definition2.setLazyInit(true); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - initMethodName이 다른 경우") + void beanDefinitionEquals_DifferentInitMethodName() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + definition1.setInitMethodName("init"); + + BeanDefinition definition2 = new BeanDefinition(UserService.class); + definition2.setInitMethodName("initialize"); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + } + + @Test + @DisplayName("BeanDefinition의 equals 테스트 - destroyMethodName이 다른 경우") + void beanDefinitionEquals_DifferentDestroyMethodName() { + // given + BeanDefinition definition1 = new BeanDefinition(UserService.class); + definition1.setDestroyMethodName("cleanup"); + + BeanDefinition definition2 = new BeanDefinition(UserService.class); + definition2.setDestroyMethodName("destroy"); + + // when & then + assertThat(definition1).isNotEqualTo(definition2); + } + + @Test + @DisplayName("BeanDefinition 생성 시 null 클래스 전달하면 예외 발생") + void beanDefinitionConstructor_NullClass() { + // when & then + assertThatThrownBy(() -> new BeanDefinition(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/boa/src/test/java/beans/factory/support/AbstractAutowireCapableBeanFactoryTest.java b/boa/src/test/java/beans/factory/support/AbstractAutowireCapableBeanFactoryTest.java new file mode 100644 index 0000000..bc998bb --- /dev/null +++ b/boa/src/test/java/beans/factory/support/AbstractAutowireCapableBeanFactoryTest.java @@ -0,0 +1,231 @@ +package beans.factory.support; + +import beans.BeansException; +import beans.PropertyValue; +import beans.PropertyValues; +import beans.factory.config.BeanDefinition; +import beans.factory.config.BeanReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.assertj.core.api.Assertions.*; + +/** + * AbstractAutowireCapableBeanFactory의 핵심 기능인 빈 생성 과정을 테스트합니다. + * - 빈 인스턴스화 + * - 프로퍼티 주입 + * - 빈 초기화 + * 다른 테스트 클래스와의 관계: + * - BeanLifecycleIntegrationTest: 생명주기 인터페이스들의 통합 테스트 + * - DefaultListableBeanFactoryTest: 완전한 BeanFactory 기능 테스트 + * - SimpleInstantiationStrategyTest: 인스턴스화 전략 테스트 + */ +class AbstractAutowireCapableBeanFactoryTest { + + private TestAutowireCapableBeanFactory beanFactory; + + @BeforeEach + void setUp() { + beanFactory = new TestAutowireCapableBeanFactory(); + } + + @Test + @DisplayName("populateBean 메서드가 참조가 아닌 프로퍼티 주입을 올바르게 수행해야 한다") + void testPopulateBeanWithProperties() { + // given + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("stringValue", "test")); + propertyValues.addPropertyValue(new PropertyValue("intValue", 42)); + + BeanDefinition beanDefinition = new BeanDefinition(PropertiesBean.class, propertyValues); + + // when + PropertiesBean bean = (PropertiesBean) beanFactory.createBean("beanWithProperties", beanDefinition); + + // then + assertThat(bean.getStringValue()).isEqualTo("test"); + assertThat(bean.getIntValue()).isEqualTo(42); + } + + @Test + @DisplayName("populateBean 메서드가 빈 참조 주입을 올바르게 수행해야 한다") + void testPopulateBeanWithBeanReference() { + // given + // 참조될 빈 먼저 등록 + BeanDefinition dependencyDefinition = new BeanDefinition(SimpleBean.class); + beanFactory.registerBeanDefinition("dependency", dependencyDefinition); + + // 빈 참조를 가진 빈 정의 + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("dependency", new BeanReference("dependency"))); + BeanDefinition mainDefinition = new BeanDefinition(ReferenceBean.class, propertyValues); + + // when + ReferenceBean bean = (ReferenceBean) beanFactory.createBean("mainBean", mainDefinition); + + // then + assertThat(bean.getDependency()).isNotNull(); + assertThat(bean.getDependency()).isInstanceOf(SimpleBean.class); + // 같은 인스턴스인지 확인 (싱글톤) + assertThat(bean.getDependency()).isSameAs(beanFactory.getSingleton("dependency")); + } + + @Test + @DisplayName("createBeanInstance 메서드가 InstantiationStrategy를 사용하여 빈을 생성해야 한다") + void testCreateBeanInstance() { + // given + BeanDefinition beanDefinition = new BeanDefinition(SimpleBean.class); + + // when + Object instance = beanFactory.createBeanInstance("testBean", beanDefinition); + + // then + assertThat(instance).isNotNull(); + assertThat(instance).isInstanceOf(SimpleBean.class); + } + + // === 예외 상황 테스트 === + + @Test + @DisplayName("존재하지 않는 프로퍼티 주입 시 BeansException이 발생해야 한다") + void testPopulateBeanWithInvalidProperty() { + // given + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("nonExistentField", "value")); + BeanDefinition beanDefinition = new BeanDefinition(SimpleBean.class, propertyValues); + + // when & then + assertThatThrownBy(() -> beanFactory.createBean("testBean", beanDefinition)) + .isInstanceOf(BeansException.class); + } + + @Test + @DisplayName("존재하지 않는 빈 참조 시 BeansException이 발생해야 한다") + void testPopulateBeanWithInvalidBeanReference() { + // given + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("dependency", new BeanReference("nonExistentBean"))); + BeanDefinition beanDefinition = new BeanDefinition(ReferenceBean.class, propertyValues); + + // when & then + assertThatThrownBy(() -> beanFactory.createBean("testBean", beanDefinition)) + .isInstanceOf(BeansException.class); + } + + @Test + @DisplayName("빈 생성 중 예외 발생 시 BeansException으로 래핑되어야 한다") + void testCreateBeanWithException() { + // given + BeanDefinition beanDefinition = new BeanDefinition(BeanWithExceptionInConstructor.class); + + // when & then + assertThatThrownBy(() -> beanFactory.createBean("exceptionBean", beanDefinition)) + .isInstanceOf(BeansException.class) + .hasMessageContaining("Failed to create bean"); + } + + // === 테스트용 구현체 및 헬퍼 클래스들 === + + /** + * 테스트용 AbstractAutowireCapableBeanFactory 구현체 + */ + private static class TestAutowireCapableBeanFactory extends AbstractAutowireCapableBeanFactory { + private final Map beanDefinitionMap = new ConcurrentHashMap<>(); + + @Override + protected boolean containsBeanDefinition(String beanName) { + return beanDefinitionMap.containsKey(beanName); + } + + @Override + public BeanDefinition getBeanDefinition(String beanName) throws BeansException { + BeanDefinition beanDefinition = beanDefinitionMap.get(beanName); + if (beanDefinition == null) { + throw new BeansException("No bean definition found for name: " + beanName); + } + return beanDefinition; + } + + public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) { + beanDefinitionMap.put(beanName, beanDefinition); + } + + @Override + public T getBean(Class requiredType) throws BeansException { + throw new UnsupportedOperationException("Not implemented for test"); + } + + // 테스트용 protected 메서드 노출 + @Override + public Object createBeanInstance(String beanName, BeanDefinition beanDefinition) { + return super.createBeanInstance(beanName, beanDefinition); + } + } + + /** + * 다양한 프로퍼티를 가진 테스트 빈 + */ + public static class PropertiesBean { + private String stringValue; + private int intValue; + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public int getIntValue() { + return intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + } + + /** + * 빈 참조를 가진 테스트 빈 + */ + public static class ReferenceBean { + private SimpleBean dependency; + + public SimpleBean getDependency() { + return dependency; + } + + public void setDependency(SimpleBean dependency) { + this.dependency = dependency; + } + } + + /** + * 단순한 테스트 빈 + */ + public static class SimpleBean { + private String value = "default"; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + /** + * 생성자에서 예외를 발생시키는 테스트 빈 + */ + public static class BeanWithExceptionInConstructor { + public BeanWithExceptionInConstructor() { + throw new RuntimeException("Constructor exception"); + } + } +} diff --git a/boa/src/test/java/beans/factory/support/DefaultListableBeanFactoryTest.java b/boa/src/test/java/beans/factory/support/DefaultListableBeanFactoryTest.java new file mode 100644 index 0000000..1d78d0d --- /dev/null +++ b/boa/src/test/java/beans/factory/support/DefaultListableBeanFactoryTest.java @@ -0,0 +1,289 @@ +package beans.factory.support; + +import beans.factory.UserRepository; +import beans.factory.UserService; +import beans.PropertyValue; +import beans.PropertyValues; +import beans.factory.NoSuchBeanDefinitionException; +import beans.factory.NoUniqueBeanDefinitionException; +import beans.factory.config.BeanDefinition; +import beans.factory.config.BeanReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class DefaultListableBeanFactoryTest { + + private DefaultListableBeanFactory beanFactory; + + @BeforeEach + void setUp() { + beanFactory = new DefaultListableBeanFactory(); + } + + // === 빈 생명주기 테스트 === + @Test + @DisplayName("빈이 생성된 후 싱글톤 레지스트리에 등록되어야 한다") + void testSingletonRegistry() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", beanDefinition); + + // when + Object bean1 = beanFactory.getBean("userRepository"); + + // then + // 같은 빈을 다시 요청해도 동일한 인스턴스 + Object bean2 = beanFactory.getBean("userRepository"); + assertThat(bean1).isSameAs(bean2); + } + + // === 프로토타입 스코프 테스트 === +// @Test +// @DisplayName("프로토타입 빈은 매번 새로운 인스턴스를 반환해야 한다") +// void testPrototypeBean() { +// // given +// BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); +// beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); +// beanFactory.registerBeanDefinition("userRepository", beanDefinition); +// +// // when +// Object bean1 = beanFactory.getBean("userRepository"); +// Object bean2 = beanFactory.getBean("userRepository"); +// +// // then +// assertThat(bean1).isNotNull(); +// assertThat(bean2).isNotNull(); +// assertThat(bean1).isNotSameAs(bean2); +// assertThat(bean1).isNotEqualTo(bean2); // 다른 인스턴스 +// } + + // === 의존성 주입 테스트 === + @Test + @DisplayName("PropertyValue를 통한 의존성 주입이 정상 동작해야 한다") + void testDependencyInjectionWithPropertyValue() { + // given + // UserRepository 빈 등록 + BeanDefinition repositoryDefinition = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", repositoryDefinition); + + // UserService 빈 등록 (UserRepository 의존성 주입) + PropertyValues propertyValues = new PropertyValues(); + propertyValues.addPropertyValue(new PropertyValue("userRepository", new BeanReference("userRepository"))); + BeanDefinition serviceDefinition = new BeanDefinition(UserService.class, propertyValues); + beanFactory.registerBeanDefinition("userService", serviceDefinition); + + // when + UserService userService = (UserService) beanFactory.getBean("userService"); + + // then + assertThat(userService).isNotNull(); + assertThat(userService.getUserRepository()).isNotNull(); + assertThat(userService.getUserRepository()).isInstanceOf(UserRepository.class); + } + + // === 타입으로 빈 조회 테스트 === + @Test + @DisplayName("타입으로 빈 조회 시 해당 타입의 빈을 반환해야 한다") + void testGetBeanByType() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", beanDefinition); + + // when + UserRepository userRepository = beanFactory.getBean(UserRepository.class); + + // then + assertThat(userRepository).isNotNull(); + assertThat(userRepository).isInstanceOf(UserRepository.class); + } + + @Test + @DisplayName("타입으로 빈 조회 시 해당 타입의 빈이 없으면 예외 발생") + void testGetBeanByType_NotFound() { + // when & then + assertThatThrownBy(() -> beanFactory.getBean(UserRepository.class)) + .isInstanceOf(NoSuchBeanDefinitionException.class); + } + + @Test + @DisplayName("동일 타입의 빈이 여러 개 있을 때 예외 발생") + void testGetBeanByType_MultipleBeansFound() { + // given + BeanDefinition definition1 = new BeanDefinition(UserRepository.class); + BeanDefinition definition2 = new BeanDefinition(UserRepository.class); + + beanFactory.registerBeanDefinition("userRepository1", definition1); + beanFactory.registerBeanDefinition("userRepository2", definition2); + + // when & then + assertThatThrownBy(() -> beanFactory.getBean(UserRepository.class)) + .isInstanceOf(NoUniqueBeanDefinitionException.class); + } + +// @Test +// @DisplayName("타입으로 해당 타입의 모든 빈 이름을 조회할 수 있어야 한다") +// void testGetBeanNamesForType() { +// // given +// BeanDefinition definition1 = new BeanDefinition(UserRepository.class); +// BeanDefinition definition2 = new BeanDefinition(UserRepository.class); +// BeanDefinition serviceDefinition = new BeanDefinition(UserService.class); +// +// beanFactory.registerBeanDefinition("userRepository1", definition1); +// beanFactory.registerBeanDefinition("userRepository2", definition2); +// beanFactory.registerBeanDefinition("userService", serviceDefinition); +// +// // when +// String[] beanNames = beanFactory.getBeanNamesForType(UserRepository.class); +// +// // then +// assertThat(beanNames).hasSize(2); +// assertThat(beanNames).containsExactlyInAnyOrder("userRepository1", "userRepository2"); +// } + + // === 예외 상황 테스트 === + @Test + @DisplayName("존재하지 않는 빈 이름으로 조회 시 NoSuchBeanDefinitionException 발생") + void testGetBean_BeanNotFound() { + // when & then + assertThatThrownBy(() -> beanFactory.getBean("nonExistentBean")) + .isInstanceOf(NoSuchBeanDefinitionException.class); + } + + @Test + @DisplayName("존재하지 않는 빈 정의 조회 시 NoSuchBeanDefinitionException 발생") + void testGetBeanDefinition_NotFound() { + // when & then + assertThatThrownBy(() -> beanFactory.getBeanDefinition("nonExistentBean")) + .isInstanceOf(NoSuchBeanDefinitionException.class); + } + + + // === 복잡한 의존성 그래프 테스트 === + @Test + @DisplayName("다단계 의존성 주입이 정상 동작해야 한다") + void testMultiLevelDependencyInjection() { + // given + // Repository 등록 + BeanDefinition repositoryDefinition = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", repositoryDefinition); + + // Service 등록 (Repository 의존성) + PropertyValues serviceProperties = new PropertyValues(); + serviceProperties.addPropertyValue(new PropertyValue("userRepository", new BeanReference("userRepository"))); + BeanDefinition serviceDefinition = new BeanDefinition(UserService.class, serviceProperties); + beanFactory.registerBeanDefinition("userService", serviceDefinition); + + // when + UserService userService = (UserService) beanFactory.getBean("userService"); + + // then + assertThat(userService).isNotNull(); + assertThat(userService.getUserRepository()).isNotNull(); + assertThat(userService.getUserRepository()).isSameAs(beanFactory.getBean("userRepository")); + } + + // === 빈 정의 덮어쓰기 테스트 === + @Test + @DisplayName("동일한 이름으로 빈 정의를 재등록하면 새로운 정의로 덮어써야 한다") + void testBeanDefinitionOverride() { + // given + BeanDefinition originalDefinition = new BeanDefinition(UserRepository.class); + originalDefinition.setScope(BeanDefinition.SCOPE_SINGLETON); + beanFactory.registerBeanDefinition("userRepository", originalDefinition); + + // when + BeanDefinition newDefinition = new BeanDefinition(UserRepository.class); + newDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("userRepository", newDefinition); + + // then + BeanDefinition retrievedDefinition = beanFactory.getBeanDefinition("userRepository"); + assertThat(retrievedDefinition.isPrototype()).isTrue(); + assertThat(retrievedDefinition.isSingleton()).isFalse(); + } + + // === null 안전성 테스트 === + @Test + @DisplayName("null 빈 이름으로 등록 시 예외 발생") + void testRegisterBeanDefinition_NullBeanName() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); + + // when & then + assertThatThrownBy(() -> beanFactory.registerBeanDefinition(null, beanDefinition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Bean name must not be null"); + } + + @Test + @DisplayName("빈 문자열 빈 이름으로 등록 시 예외 발생") + void testRegisterBeanDefinition_EmptyBeanName() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); + + // when & then + assertThatThrownBy(() -> beanFactory.registerBeanDefinition("", beanDefinition)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("null BeanDefinition 등록 시 예외 발생") + void testRegisterBeanDefinition_NullBeanDefinition() { + // when & then + assertThatThrownBy(() -> beanFactory.registerBeanDefinition("userRepository", null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("빈 정의가 존재하는지 확인할 수 있어야 한다") + void testContainsBeanDefinition() { + // given + BeanDefinition beanDefinition = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", beanDefinition); + + // when + boolean contains = beanFactory.containsBeanDefinition("userRepository"); + + // then + assertThat(contains).isTrue(); + } + + @Test + @DisplayName("빈 정의가 존재하지 않는 경우 false를 반환해야 한다") + void testContainsBeanDefinitionNotFound() { + // when + boolean contains = beanFactory.containsBeanDefinition("nonExistentBean"); + + // then + assertThat(contains).isFalse(); + } + + @Test + @DisplayName("빈 정의 이름을 배열로 반환할 수 있어야 한다") + void testGetBeanDefinitionNames() { + // given + BeanDefinition beanDefinition1 = new BeanDefinition(UserRepository.class); + beanFactory.registerBeanDefinition("userRepository", beanDefinition1); + BeanDefinition beanDefinition2 = new BeanDefinition(UserService.class); + beanFactory.registerBeanDefinition("userService", beanDefinition2); + + // when + String[] beanNames = beanFactory.getBeanDefinitionNames(); + + // then + assertThat(beanNames).contains("userRepository", "userService"); + } + + @Test + @DisplayName("빈 정의가 없는 경우 빈 이름 배열은 비어 있어야 한다") + void testGetBeanDefinitionNamesEmpty() { + // when + String[] beanNames = beanFactory.getBeanDefinitionNames(); + + // then + assertThat(beanNames).isEmpty(); + } +} diff --git a/boa/src/test/java/beans/factory/support/DefaultSingletonBeanRegistryTest.java b/boa/src/test/java/beans/factory/support/DefaultSingletonBeanRegistryTest.java new file mode 100644 index 0000000..f4ceecc --- /dev/null +++ b/boa/src/test/java/beans/factory/support/DefaultSingletonBeanRegistryTest.java @@ -0,0 +1,168 @@ +package beans.factory.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class DefaultSingletonBeanRegistryTest { + + private DefaultSingletonBeanRegistry registry; + + @BeforeEach + void setUp() { + registry = new DefaultSingletonBeanRegistry(); + } + + @Test + @DisplayName("싱글톤 객체를 등록하고 조회할 수 있어야 한다") + void testAddAndGetSingleton() { + // given + String beanName = "testBean"; + Object testObject = new TestBean("test"); + + // when + registry.addSingleton(beanName, testObject); + Object retrieved = registry.getSingleton(beanName); + + // then + assertThat(retrieved).isNotNull().isSameAs(testObject); + } + + @Test + @DisplayName("등록되지 않은 싱글톤 조회 시 null을 반환해야 한다") + void testGetNonExistentSingleton() { + // when + Object retrieved = registry.getSingleton("nonExistentBean"); + + // then + assertThat(retrieved).isNull(); + } + + @Test + @DisplayName("동일한 이름으로 여러 번 등록하면 마지막 객체가 반환되어야 한다") + void testOverrideSingleton() { + // given + String beanName = "testBean"; + Object firstObject = new TestBean("first"); + Object secondObject = new TestBean("second"); + + // when + registry.addSingleton(beanName, firstObject); + registry.addSingleton(beanName, secondObject); + Object retrieved = registry.getSingleton(beanName); + + // then + assertThat(retrieved).isSameAs(secondObject) + .isNotSameAs(firstObject); + } + + @Test + @DisplayName("여러 개의 싱글톤을 동시에 관리할 수 있어야 한다") + void testMultipleSingletons() { + // given + String beanName1 = "bean1"; + String beanName2 = "bean2"; + String beanName3 = "bean3"; + Object object1 = new TestBean("test1"); + Object object2 = new TestBean("test2"); + Object object3 = new TestBean("test3"); + + // when + registry.addSingleton(beanName1, object1); + registry.addSingleton(beanName2, object2); + registry.addSingleton(beanName3, object3); + + // then + assertThat(registry.getSingleton(beanName1)).isSameAs(object1); + assertThat(registry.getSingleton(beanName2)).isSameAs(object2); + assertThat(registry.getSingleton(beanName3)).isSameAs(object3); + } + + @Test + @DisplayName("동시에 여러 스레드에서 접근해도 안전해야 한다") + void testConcurrentAccess() throws InterruptedException { + // given + int threadCount = 10; + Thread[] threads = new Thread[threadCount]; + + // when + for (int i = 0; i < threadCount; i++) { + final int threadIndex = i; + threads[i] = new Thread(() -> { + String beanName = "bean" + threadIndex; + Object testObject = new TestBean("test" + threadIndex); + registry.addSingleton(beanName, testObject); + + // 등록한 객체가 올바르게 조회되는지 확인 + Object retrieved = registry.getSingleton(beanName); + assertThat(retrieved).isSameAs(testObject); + }); + } + + // 모든 스레드 시작 + for (Thread thread : threads) { + thread.start(); + } + + // 모든 스레드 완료 대기 + for (Thread thread : threads) { + thread.join(); + } + + // then - 모든 빈이 올바르게 등록되었는지 확인 + for (int i = 0; i < threadCount; i++) { + String beanName = "bean" + i; + Object retrieved = registry.getSingleton(beanName); + assertThat(retrieved).isNotNull(); + assertThat(((TestBean) retrieved).getValue()).isEqualTo("test" + i); + } + } + + @Test + @DisplayName("특수 문자가 포함된 빈 이름도 처리할 수 있어야 한다") + void testSpecialCharacterBeanNames() { + // given + String[] specialNames = { + "bean.with.dots", + "bean-with-dashes", + "bean_with_underscores", + "bean$with$dollars", + "bean@with@at", + "한글빈이름" + }; + + // when & then + for (int i = 0; i < specialNames.length; i++) { + String beanName = specialNames[i]; + Object testObject = new TestBean("test" + i); + + registry.addSingleton(beanName, testObject); + Object retrieved = registry.getSingleton(beanName); + + assertThat(retrieved).isSameAs(testObject); + } + } + + + /** + * 테스트용 간단한 빈 클래스 + */ + private static class TestBean { + private final String value; + + public TestBean(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "TestBean{value='" + value + "'}"; + } + } +} diff --git a/boa/src/test/java/beans/factory/support/SimpleInstantiationStrategyTest.java b/boa/src/test/java/beans/factory/support/SimpleInstantiationStrategyTest.java new file mode 100644 index 0000000..e9915b2 --- /dev/null +++ b/boa/src/test/java/beans/factory/support/SimpleInstantiationStrategyTest.java @@ -0,0 +1,184 @@ +package beans.factory.support; + +import beans.BeansException; +import beans.factory.BeanFactory; +import beans.factory.config.BeanDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SimpleInstantiationStrategyTest { + + private SimpleInstantiationStrategy instantiationStrategy; + + @Mock + private BeanFactory mockBeanFactory; + + @BeforeEach + void setUp() { + instantiationStrategy = new SimpleInstantiationStrategy(); + } + + @Test + @DisplayName("기본 생성자가 있는 클래스의 인스턴스를 생성할 수 있어야 한다") + void testInstantiateWithDefaultConstructor() { + // given + BeanDefinition beanDefinition = new BeanDefinition(SimpleTestBean.class); + + // when + Object instance = instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory); + + // then + assertThat(instance).isNotNull(); + assertThat(instance).isInstanceOf(SimpleTestBean.class); + assertThat(((SimpleTestBean) instance).getValue()).isEqualTo("default"); + } + + @Test + @DisplayName("매번 새로운 인스턴스를 생성해야 한다") + void testInstantiateCreatesNewInstance() { + // given + BeanDefinition beanDefinition = new BeanDefinition(SimpleTestBean.class); + + // when + Object instance1 = instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory); + Object instance2 = instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory); + + // then + assertThat(instance1).isNotNull(); + assertThat(instance2).isNotNull(); + assertThat(instance1).isNotSameAs(instance2); + assertThat(instance1).isNotEqualTo(instance2); + } + + // === 예외 상황 테스트 === + @Test + @DisplayName("기본 생성자가 없는 클래스 인스턴스화 시 예외가 발생해야 한다") + // TODO: 기본 생성자가 없어도 다른 생성자가 있다면 인스턴스화할 수 있어야 함(매개변수가 있는 생성자 사용) + void testInstantiateWithoutDefaultConstructor() { + // given + BeanDefinition beanDefinition = new BeanDefinition(BeanWithoutDefaultConstructor.class); + + // when & then + assertThatThrownBy(() -> instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory)) + .isInstanceOf(BeansException.class); + } + + @Test + @DisplayName("추상 클래스 인스턴스화 시 예외가 발생해야 한다") + void testInstantiateAbstractClass() { + // given + BeanDefinition beanDefinition = new BeanDefinition(AbstractTestBean.class); + + // when & then + assertThatThrownBy(() -> instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory)) + .isInstanceOf(BeansException.class); + } + + @Test + @DisplayName("인터페이스 인스턴스화 시 예외가 발생해야 한다") + void testInstantiateInterface() { + // given + BeanDefinition beanDefinition = new BeanDefinition(TestInterface.class); + + // when & then + assertThatThrownBy(() -> instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory)) + .isInstanceOf(BeansException.class); + } + + @Test + @DisplayName("생성자에서 예외가 발생하는 클래스 인스턴스화 시 예외가 발생해야 한다") + void testInstantiateWithConstructorException() { + // given + BeanDefinition beanDefinition = new BeanDefinition(BeanWithExceptionInConstructor.class); + + // when & then + assertThatThrownBy(() -> instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory)) + .isInstanceOf(BeansException.class); + } + + // === 특수한 경우 테스트 === + @Test + @DisplayName("private 기본 생성자가 있는 클래스는 인스턴스화할 수 없어야 한다") + void testInstantiateWithPrivateConstructor() { + // given + BeanDefinition beanDefinition = new BeanDefinition(BeanWithPrivateConstructor.class); + + // when & then + assertThatThrownBy(() -> instantiationStrategy.instantiate(beanDefinition, "testBean", mockBeanFactory)) + .isInstanceOf(BeansException.class); + } + + // === 테스트용 클래스들 === + /** + * 기본 생성자가 있는 단순한 테스트 빈 + */ + public static class SimpleTestBean { + private String value = "default"; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + /** + * 기본 생성자가 없는 테스트 빈 + */ + public static class BeanWithoutDefaultConstructor { + private final String value; + + public BeanWithoutDefaultConstructor(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + /** + * 추상 클래스 + */ + public static abstract class AbstractTestBean { + public abstract void doSomething(); + } + + /** + * 테스트 인터페이스 + */ + public interface TestInterface { + void doSomething(); + } + + /** + * 생성자에서 예외가 발생하는 테스트 빈 + */ + public static class BeanWithExceptionInConstructor { + public BeanWithExceptionInConstructor() { + throw new RuntimeException("Constructor exception"); + } + } + + /** + * private 생성자를 가진 테스트 빈 + */ + public static class BeanWithPrivateConstructor { + private String value = "private"; + + private BeanWithPrivateConstructor() { + // private constructor + } + + public String getValue() { + return value; + } + } +}