스프링에서 비동기로 작성 된 로직은 다른 동기 로직과 상호작용을 테스트 하기 불편하다.
@Transactional 어노테이션과 함께 테스트 할 시, 스레드가 달라 트랜잭션 범위가 다르기 때문에, uncommit 된 레코드를 읽을 수 없다.
설명 전, 테스트 클래스에서 @Transactional 어노테이션은
그렇다면 비동기 매서드는 어떻게 동작할까? 스레드가 다르다면?
예를 들어
@BeforeEach
void setUp() {
user = new User("test", "test", AllowedUser.JOJO.getName(), AllowedUser.JOJO.getRegNo());
userJpaRepository.save(yoobi);
}
...
이런 셋업 매서드가 있을 시, 비동기 스레드는 이 셋업 매서드를 알지 못한다.
위에서 얘기했듯, 테스트 매서드는 하나의 스레드가 영속성 컨텍스트를 공유하고, 이에 따라 모든 매서드가 끝난 후, 즉 모든 작업 후 커밋/롤백 된다고 얘기했다.
( 즉, 셋업 매서드가 작동할 수 있는 것은 영속성 컨텍스트 덕분이다 )
그러나 스레드가 다르고, 다른 영속성 컨텍스트에서 트랜잭션을 관리하려고 한다면
와 같은 형태로 영속성 컨텍스트의 범위가 나뉘어지게 되고, 아래 단계를 밟게 된다.
불편하다.
만약 외부 api 호출이 오래걸린다고 생각해보자 (타임아웃 : 1분)
따라서 우리는 호출부를 비동기로 작성했고, 이 비동기 매서드는 다른 스레드에서 작성되므로 이에 대한 테스트는
void 슈도테스트(){
1. 서비스 매서드 호출
2. 비동기 매서드 호출
3. 비동기 매서드 호출 타임아웃을 기다려야함. (~1분) -> CountDownLatch 등 활용
...
}
이런 식일 것이다.
즉, 호출과 응답이 1분 이전에 왔다고 하더라도 테스트 매서드는 이를 알 수 없으므로, Read timeout 인 1분까지 기다려야 한다는 단점이 있다.
불편하다.
@ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests. Specifically, @ContextConfiguration declares the application context resource locations or the component classes used to load the context.
공식문서에서는 위와 같이, 통합 테스트 환경에서 어플리케이션 컨텍스트를 어떻게 load 할 것인지에 대한 설정을 @ContextConfiguration 어노테이션을 통해서 할 수 있다고 명시되어 있다.
운영 환경에서 @Asnyc 어노테이션이 이 TaskExecutor 를 통해서 동작하므로, 테스트 환경에서의 컨피규레이션은 동기 처리되는 SyncTaskExecutor 로 바꿔보자.
@ContextConfiguration
public class TextContextConfiguration {
@Configuration
static class TestConfig {
@Bean
public TaskExecutor taskExecutor() {
return new SyncTaskExecutor();
}
}
}
위 설정으로 인해
@ContextConfiguration, @SpringApplicationConfiguration 등은 모두 어플리케이션 컨텍스트를 등록하는 역할을 하며, 우리가 잘 알고 있듯 @SpringBootTest 라는 어노테이션으로 대체되어(간단히 설정 가능하도록 변경되어) 있다. 따라서 테스트 클래스에 @TestConfiguration 을 추가하는 방식으로 설정이 가능하다.
@SpringBootTest
public class SimpleAsyncTest {
@TestConfiguration
static class TestConfig {
@Bean
public TaskExecutor taskExecutor() {
return new SyncTaskExecutor();
}
}
// 실제 테스트들 ...
}
혹은 이걸 커스텀 어노테이션으로 빼도 괜찮을 것 같다. 아래와 같이.
@TestConfiguration
public class TestAsyncConfig {
@Bean
public TaskExecutor taskExecutor(){
return new SyncTaskExecutor();
}
}
// ...
@SpringBootTest
@Import({TestAsyncConfig.class})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableAsyncTest {
}
// ...
@EnableAsyncTest
public class SimpleAsyncTest {
// 실제 테스트들 ...
}