Spring Integration Test
안녕하세요 ㅎㅎ
오랜만에 찾아 왔네요.
오늘은 많은 분들이 Spring 을 처음 시작하시고, 여러가지 테스트를 작성할 때 많은 어려움을 겪을 때가 많습니다.
단위테스트는 쉽게 작성해주시지만,
Spring Context 가 올라가는 @SpringBootTest 를 활용한 IntegrationTest 를 작성할 때 부터는 많은 어려움들이 있는데요.
오늘은 IntegrationTest 를 작성할 때 많이 쓰이는 모든 것들에 대해 알아보도록 하겠습니다.
Integration Test
우선은 IntegrationTest 가 무엇이고 어떤 것인지 보겠습니다.
IntegrationTest 는 Unit Test(단위테스트) 보다는 조금 더 리얼 환경에 가까운 테스트로,
배포되는 서버를 기준으로 외부 자원(DB, External API, ...) 들을 포함한 테스트를 의미합니다.
즉 IntegrationTest 는 외부 자원들을 mock 하는 것이 아닌 실제 환경과 유사한 환경을 구축하여 테스트를 진행하게 되는 것이죠.
그래서, IntegrationTest 는
DB CRUD 테스트를 진행하거나,
Controller 에게 요청을 보낸다거나,
External API 를 직접 호출한다거나
서버 자원에 대해 직접적으로 의존하게 됩니다.
다만, UI Test와 다른점은 실제 User 가 비즈니스 Action 들을 취하는 것이 아닌,
특정한 상황을 가정하여 진행하게 되는 것이죠.
따라서 Unit Test 보다는 조금 더 무겁지만,
더 안정적인 비즈니스 서버 운영을 위해 테스트를 가동한다. 라고 이해하시면 될 것 같아요.
그러다보니 IntegrationTest 는
테스트가 많아지면 많아질수록 가동이 오래걸리고,
Spring Context 를 계속 띄우다보면 오버헤드가 발생하기에
최소한의 Spring Context 자원으로 테스트를 운영해야 합니다.
우리는 이제 어떻게 Spring Context 자원들을 운영하는 테스트를 작성할 것인지에 대하여 알아보도록 하겠습니다.
빠른 Integration Test 를 위해서는 적절한 Package 구조 혹은 Module 구조 가 필요하다
왜 그런가 하면, Spring Context 에 포함하는 Bean 들을 줄이기 위함입니다.
예를 들면, External API 를 호출하는 테스트에서는 DB Connection 을 맺는 Bean 은 필요없고,
반대로 DB 테스트를 하는데 External API 와 Connection 을 맺을 이유는 없기 때문입니다.
제가 단순히 테스트 때문이라고는 했지만, 사실은 Hexagonal Architecture 와 연관이 있습니다.
핵심 비즈니스 로직은 외부 연동과 의존성을 갖지 않게 Layer 구조를 잡기 위해서는
Domain 영역에 Persistence 영역이 들어가면 안되고,
마찬가지로 Kafka 관련한 Event Producer Event Consumer 도 Domain 영역에 속하면 안됩니다.
( 저는 테스트를 계속 설명해야 하므로 ㅎㅎ;; 설명은 다른 훌륭한 개발자분이 정리하신 글을 참고해주세요 )
https://mesh.dev/20210910-dev-notes-007-hexagonal-architecture/
공통적인 Spring Context 를 추상화하자
위에서 언급하였듯이 공통적인 Spring Context 를 정의하는 것이 중요합니다.
제가 생각하는 속성들은 아래와 같습니다.
- DB Context -> Persistence Layer
- External API Context -> External API Layer
- RestController Context -> Controller Layer
보통의 경우에는 3가지의 속성들이 있고, 비즈니스 요건에 따라 종류는 더 있을 수도 있을 것 같아요.
이번 게시글에서는 External API ( Feign 을 사용한 ) Context 를 예시로 한번 살펴볼게요.
기본적으로 위와 같이 Layer 구조를 잡고, 아래와 같이 Abstract Class 를 만들어봅니다.
import com.huisam.kotlinweb.fegin.FeignPackage
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
@EnableRetry(proxyTargetClass = true)
@Configuration
@EnableFeignClients(basePackageClasses = [FeignPackage::class])
@ComponentScan(basePackageClasses = [FeignPackage::class])
class FeignClientConfiguration // Production 에 FeignClient 설정
---
import com.fasterxml.jackson.databind.ObjectMapper
import com.huisam.kotlinweb.fegin.config.AutoConfigureTestFeign
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.test.context.ContextConfiguration
import com.fasterxml.jackson.databind.ObjectMapper
import com.huisam.kotlinweb.fegin.config.AutoConfigureTestFeign
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.TypeExcludeFilter
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.FilterType
import org.springframework.test.context.ContextConfiguration
@TestConfiguration
@ComponentScan(
basePackageClasses = [FeignPackage::class], // 1. scan 대상의 패키지
excludeFilters = [ComponentScan.Filter(type = FilterType.CUSTOM, classes = [TypeExcludeFilter::class])] // 2. TestComponent 는 스캔 제외 허용
)
class FeignTestContextConfiguration
@ContextConfiguration(classes = [FeignTestContextConfiguration::class]) // 3. test context 정의
@AutoConfigureTestFeign // 4. 관련 설정 Import 들은 하나로 응집
@SpringBootTest
abstract class AbstractFeignContractTest {
@Autowired
protected lateinit var objectMapper: ObjectMapper
}
가장 중요한점은 @ContextConfiguration 와 ComponentScan 을 활용해서 필요한 Bean 들에 대한 Load 를 진행한 것에 있습니다.
그리고 이 추상화된 클래스는 각각의 테스트마다 적절하게 활용할 수 있게 되는 것이죠.
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration
import org.springframework.cloud.openfeign.FeignAutoConfiguration
import org.springframework.context.annotation.Import
@Import(HttpClientConfiguration::class)
@ImportAutoConfiguration(
value = [FeignAutoConfiguration::class]
)
@AutoConfigureJson
annotation class AutoConfigureTestFeign
( 혹여나 AutoConfigure 클래스도 궁금하실까 하여 같이 올려드립니다 ㅎㅎ )
위의 예시로는 저같은 경우에는 Annotation 으로 Import 해야되는 설정들을 모아놓았지만,
실제 구현하실 때는 각각의 상황에 맞는 Annotation 으로 사용하시길 바랍니다.
위 AbstractClass 를 기반으로 실제 테스트는 위 추상클래스를 상속해서 테스트를 작성해주시면 됩니다.
internal class RemotePlaceHolderClientContractIntegrationTest : AbstractFeignContractTest() {
@Autowired
private lateinit var client: RemotePlaceHolderClient
@Test
fun `제대로 post 들을 가져온다`() {
// given & when
val result = client.posts()
// then
assertThat(result).isNotEmpty
}
@Test
fun `제대로 post 1번을 가져온다`() {
// given
val postId = 1L
// when
val result = client.post(postId)
// then
assertThat(result.id).isEqualTo(1L)
}
}
Test 관련 Annotation 소개
위에서 작성한 코드가 이해가 안되신다면 아직 Annotation 관련해서 생소해서 그럴 것이라 생각합니다.
Annotation | 설명 |
@ContextConfiguration | IntegrationTest 를 위해서 어떤 Bean 들을 로드하고 관리할 것인지 ApplicationContext 를 설정하는 기준을 만들어주는 Configuration |
@Import | @Configuration 또는 @TestConfiguration 의 설정을 로드할 수 있는 역할 |
@ImportAutoConfiguration | AutoConfigure 를 로드해주는 역할 |
@TestConfiguration | @SpringBootConfiguration 의 scan 대상으로는 포함하지는 않는 대신, Configuration 과 똑같은 역할. Test 를 위한 Configuration |
@TestComponent | @SpringBootConfiguration 의 scan 대상으로는 포함하지는 않는 대신, Component 과 똑같은 역할. Test 를 위한 Component 이지만, javadoc 설명처럼 직접적으로 ComponentScan 을 사용하는 경우 Filter 에 대한 정의를 해주어야함. |
위에 있는 Annotation 말고도 종류는 엄청 많지만, 설명이 필요한 부분들만 작성하였어요 ㅎㅎ
테스트를 설정하다보면 특이하게도, Bean 설정이 중복으로 define 되거나 로드가 되지 않는 현상들이 발생합니다.
이럴 때는 적절하게 Test Annotation 을 활용함으로써 해결할 수 있습니다.
Spring 문서를 참고해서 상황에 맞게 해결하도록 해요 ㅎㅎ
정리하며
Spring Test Context 는 계속 로드할 수록 오버헤드가 많이 커지게 됩니다.
그래서 어떻게 하면 1번의 Context 를 유지함으로 여러 테스트들을 가동하는 방법에 대해 알아보았어요.
- 효과적인 테스트를 위하여, Production 의 Layer 구조를 올바르게 잡자. ( Ex. Hexagonal Architecture )
- ContextConfiguration 과 ComponentScan 및 Import 을 적절히 활용하여, 필요한 Bean 들을 로드하자
- 하나의 AbstractClass 에 설정들을 정의하고, 구현체에서는 테스트를 작성하자
참고 자료
Practical Application of Test Pyramid in Spring-based Microservice
Testing in Spring Boot
Testing with Spring Boot’s @TestConfiguration Annotation
Spring Testing Annotations
'Developer > Spring' 카테고리의 다른 글
Spring WebFlux 에서는 어떻게 Kotlin Coroutine 을 지원하고 있을까? ( feat. Context ) (3) | 2023.05.27 |
---|---|
[JPA] Hibernate 에서 Query statement caching 은 어떻게 이루어질까? (5) | 2023.01.08 |
[Kotlin/Feign] Apache Http Client5 에 대해 알아보자 (0) | 2021.11.06 |
[JPA] DataSource 를 연결하는 방법 & RoutingDataSource 설정 (2) | 2021.06.19 |
Spring Batch란? - 기본 요소에 대해 알아보자 (0) | 2021.06.12 |