JUnit5의 병렬 실행 설정으로 인해 Mock 테스트가 예상과 다르게 동작하는 현상에 대한 분석입니다.
Intellij를 이용하여 개발을 진행하던 중 웹 계층에 대한 단위 테스트를 수행하기 위해 서비스 계층을 Mock으로 활용하여 테스트를 진행하였으나 IDE에서 실행한 테스트와 ./gradlew test
명령어를 이용한 테스트의 결과가 다르게 나오는 현상이 발생했습니다.
build.gradle 설정
...생략
test {
useJUnitPlatform()
// 병렬 처리 설정
systemProperty 'junit.jupiter.execution.parallel.enabled', 'true'
systemProperty 'junit.jupiter.execution.parallel.mode.default', 'concurrent'
}
테스트는 병렬로 진행하기 위해 위와 같이 설정을 진행한 상태였습니다. 왜 IDE에서는 문제 없이 잘 실행되는데 gradle을 사용한 테스트를 수행하면 안되는건지 이해를 할 수 없던 상황에서 한가지 의심이 들었습니다.
gradle의 설정은 IDE에 대한 설정까지 지원하지 않기 때문에 테스트 병렬에 대한 설정이 적용되지 않을 것이라 생각했고, 그로 인해 IDE는 병렬이 아니라 순차적인 테스트 실행을 하기 때문에 성공했을 것이라 판단했습니다.
이렇게 추정된 원인이 제대로 들어 맞았는지 확인해봅시다.🧐
@MockitoBean
private UpdateMemberUseCase updateMemberUseCase;
병렬 환경에서 UseCase 객체를 MockBean으로 설정하면 아래와 같은 문제가 발생할 수 있습니다.
// 테스트 A에서 설정
given(updateMemberUseCase.updateMember(any(UpdateMemberCommand.class))).willReturn(1L);
// 테스트 B가 동시에 실행되면서 Mock 상태를 변경
given(updateMemberUseCase.updateMember(any(UpdateMemberCommand.class))).willReturn(2L);
동일한 Mock 객체가 서로 다른 테스트에서 동시 실행될 경우 아래와 같이 의도치 않은 동작을 유발할 수 있습니다.
memberId
가 0(기본값)으로 반환되는 현상이 발생할 수 있다.@WebMvcTest(MemberController.class)
@WebMvcTest
는 Spring Test Context를 생성하고 캐싱합니다. 이런 특성 때문에 병렬 실행 시 여러 테스트가 같은 Context를 공유하면서 Mock 상태가 꼬이게 되고 결론적으로 Context pollution 현상이 발생 할 수 있습니다.
그래서 어떻게 해결해야 할까요?
개발 속도가 중요하고, 병렬 실행이 반드시 필요한 상황은 아니었기에 저는 간편하게 병렬 실행 설정을 제거하여 해결했습니다.
test {
useJUnitPlatform()
// 아래 두 줄 제거
// systemProperty 'junit.jupiter.execution.parallel.enabled', 'true'
// systemProperty 'junit.jupiter.execution.parallel.mode.default', 'concurrent'
}
병렬 실행이 반드시 필요하거나 적용해보고 싶으시다면 아래와 같이 reset()
메서드를 이용하여 매 테스트마다 Mock을 초기화하여 상태를 격리하는 방법이 있습니다.
@BeforeEach
void setUp() {
// Mock 초기화로 상태 격리
reset(updateMemberUseCase);
// 각 테스트마다 새로운 Mock 설정
given(updateMemberUseCase.updateMember(any(UpdateMemberCommand.class)))
.willReturn(1L);
}
또는 병렬 실행의 수행 범위를 제한하는 방법이 있지만 이 방식 또한 IDE와 설정이 일치하지 않기 때문에 의도치 않은 동작을 유발할 가능성이 있습니다.
test {
useJUnitPlatform()
systemProperty 'junit.jupiter.execution.parallel.enabled', 'true'
systemProperty 'junit.jupiter.execution.parallel.mode.default', 'same_thread' // 클래스별로만 병렬
systemProperty 'junit.jupiter.execution.parallel.mode.classes.default', 'concurrent'
}
결론적으로 원인으로 추정했던 IDE와 Gradle의 설정 차이에서 발생한 문제가 맞았습니다. "테스트를 병렬로 처리하고 싶다."라는 하나의 생각만 가지고, 간편한 설정을 통해 목적을 달성하려 했지만 전체적인 환경에 대한 고려는 되지 못한 상태여서 이런 트러블이 발생했다고 생각이 듭니다.😂
@TestMethodOrder(OrderAnnotation.class)
class MemberControllerTest {
@BeforeEach
void setUp() {
// 모든 Mock 초기화
reset(createMemberUseCase, findMemberUseCase, updateMemberUseCase,
deleteMemberUseCase, resetPasswordUseCase, unlockMemberUseCase);
}
@Test
void updateMember_Success() {
// 테스트별로 독립적인 Mock 설정
given(updateMemberUseCase.updateMember(any(UpdateMemberCommand.class)))
.willReturn(1L);
// 테스트 로직...
}
}
Mock을 사용하는 테스트에서는 병렬 실행을 신중하게 고려해야 하기 때문에 아래 항목을 고려해서 진행해야 합니다.
병렬 실행은 테스트 수행 시간을 단축시키지만, Mock을 사용하는 Spring Boot 테스트에서는 예상치 못한 부작용을 일으킬 수 있습니다. 프로젝트의 특성에 맞게 다음 중 하나를 선택하는 것이 좋습니다: