본문 바로가기
스프링부트

스프링부트 테스트(2) - configuration 과 property

by pius712 2024. 7. 23.

1. 무엇으로 configuration 을 구성할까?

참고로 조금 더 자세한 추가적인 내용은 공식문서에 있습니다.

1편에서 소개한 TestContextBootstrapper 구현체 SpringBootTestContextBootstrapper 는 기본적으로 별다른 조건이 없다면 @SpringBootConfiguration 어노테이션이 붙은 클래스를 찾아서 context 를 구성한다.

즉 @SpringBootApplication 어노테이션 내에 @SpringBootConfiguration 어노테이션을 포함하기 때문에 별다른 조건이 없는 경우 전체 컨테이너 환경이 구성되는 것이다.

// @SpringBootApplication 어노테이션
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication

하지만, 위에서 언급한 것처럼 특정 조건이 있을 때는 context 가 다르게 동작할 수 있다.

TestContextBootStrapper 에서 Configuration class 를 가져올 때 아래의 동작을한다.

  1. 수행되는 테스트 클래스 내부에 inner class 에 @Configuration 이 있는 경우
    1. 해당 Configuration 만을 사용한다.
  2. inner class 에 @TestConfiguration 이 있는 경우, SpringBootConfiguration 과 병합한다.

따라서, 대표적으로 아래의 케이스에서 다르게 동작한다.

  • nested @Configuration
  • @TestConfiguration

1.1 Nested @Configuration

nested configuration 클래스가 있는 경우, 해당 configuration 만 적용된다.

아예 spring boot context 자체가 생성되지 않고, nested configuration 으로 생성되는 것이다.

따라서, 아래의 코드는 Configuration 클래스에 TestService 가 정의되지 않았기 때문에 Bean 을 찾지 못해서 에러가 난다.

// src/main 에 구현된 테스트 대상 클래스
@Service
class TestService(
    private val testComponent: TestComponent
)

// 테스트 코드 
@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class TestMockTest(
    private val testService: TestService,
) {

    @Test
    fun test() {
        testService.test()
    }

    @Configuration
    class TestConfig {
        @Bean
        @Primary
        fun testComponent(): TestComponent {
            return mockk()
        }
    }
}

 

참고로, nested configuration 에 대해서만 이러한 동작을 하게 되고 @Import 를 통해서 구성하는 경우에는 SpringBootContext 에 추가적으로 적용된다.

@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfig::class)
class TestMockTest(
    private val testService: TestService,
) {

    @Test
    fun test() {
        testService.test()
    }
}

// 별도 파일 분리
@Configuration
class TestConfig {
    @Bean
    @Primary
    fun testComponent(): TestComponent {
        return mockk(relaxed = true)
    }
}

참고로 이 자세한 동작은 SpringBootTestContextBootstrapper.getOrFindConfigurationClasses 메서드 코드를 살펴보면 알 수 있다.

1.2 @TestConfiguration

테스트에만 사용되는 Configuration 으로, 다른 Context 에 추가로 적용되는 Configuration 이다.

아래 코드의 경우, 기존 SpringBootContext 에 추가로 TestComponent 빈을 mockk 으로 등록한다.

이 경우에는 nested configuration 과 다르게 TestService 가 잘 Inject 된다.

@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class TestMockTest(
    private val testService: TestService,
) {

    @Test
    fun test() {
        testService.test()
    }
    
    @TestConfiguration
    class TestConfig {
        @Bean
        @Primary
        fun testComponent(): TestComponent {
            return mockk(relaxed = true)
        }
    }
}

2. Property

기본적으로는 property 는 main 하위의 application.properties, application.yml 을 읽게 된다.

2.1 test 하위의 application.yml

test 하위의 application yml 은 main 의 application.yml 을 덮어쓰게 된다.

즉, main 의 application yml 은 무시하기 때문에 필요한 모든 property 를 test 하위의 application.yml 에 명시해야함.

아래 예시를 보자.

// main 에 등록
// src/main/resources/application.yml
test:
  value: app
  value2: app2

// test 에 등록
// src/test/resources/application.yml
test:
  value: test
  value2: test2
@Service
class TestService(
    @Value("\\${test.value}")
    private val testValue: String,
    @Value("\\${test.value2}")
    private val testValue2: String,
)

위의 경우, test 수행시에 test 디렉터리에 작성한 application.yml 을 읽게 된다.

하지만 이 경우에는 test 하위의 application.yml 만 읽기 때문에, 아래처럼 value2 값을 빠뜨리면 서버가 기동되지 않는다.

// src/test/resources/application.yml
test:
  value: test

2.2 profile 에 따른 동작

active profiles 를 정의하게 되면, test 하위의 application-{profile}.yml 파일을 읽게 된다.

이 경우에는 기본 application.yml 파일과 병합하게 되는데, 기존 application.yml 파일의 프로퍼티를 기본으로 하고, test 하위의 yml 파일이 재정의 할 수 있게 된다.

// src/test/resources/application-{profile}.yml
test:
  value: test
@ActiveProfiles("test")
@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class TestServicePropertyTest(
    private val testService: TestService
)

2.3 SpringBootTest 의 properties attribute 사용

properties 를 오버라이딩 할 수 있으며, yml 에 정의된 것 보다 우선권을 가지게 된다.

@SpringBootTest(properties = ["test.value=hello"])

참고로 이 경우 test 의 application context caching 이 이루어지지 않기 때문에, application context 를 추가로 로드해야한다.

Context Caching 은 3편에서 추가적으로 다룬다.

스프링 부트 테스트(3) - context caching

2.4 @TestPropertySource

위의 properties attribute 와 비슷하게 @TestPropertySource 어노테이션을 사용할 수도 있다.

이 경우에도 마찬가지로 context caching 이 되지 않는다.

@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@TestPropertySource(properties = ["datasource.connection-pool=3"])
class TestMockTest(
    private val testService: TestService,
){

    @Test
    fun test() {
        testService.test()
    }
}

2.5 property import 하기

그 외에도 property 를 import 할 수도 있다.

아래처럼 test 하위의 application.yml 에서 main 의 application.yml 을 import 할 수 있는데, 이 경우 import 한 yml 파일이 기존의 프로퍼티가 선언된 순서와 관계 없이 오버라이딩 하게 된다

// src/test/resources/application.yml
spring:
  application:
    name: junit
  config:
    import:
      - db-core.yml

// 여기서 정의한 값이 아닌 db-core.yml 값을 사용하게 된다.
test:
  value: test
  value2: test2 
// db-core.yml
test:
  value: db
  value2: db2 // 만약 이 값이 생략된다면, application.yml 값을 사용하게 된다.