테스트 코드에서 Static Method를 사용하는 경우?
진행하고 있는 프로젝트에서 프론트엔드 분의 요청으로 유저 식별을 위해
`RandomUtils.generateAlphaNumericRandomCode()`라는 static method를 사용하게 되었다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AdminCommandService {
private final AdminRepository adminRepository;
private final PasswordEncoder passwordEncoder;
private final ImageUploadService imageUploadService;
private final MailService mailService;
private final RedisService redisService;
public AdminInfoResponse create(AdminCreateRequest request){
validateEmailAndNickNameAndLoginId(request.getEmail(), request.getNickName(),
request.getLoginId());
request.modifyPassword(passwordEncoder.encode(request.getPassword()));
String urlName = RandomUtils.generateAlphaNumericRandomCode();
// Admin newAdmin = Admin.of(request, urlName);
Admin newAdmin = Admin.builder()
.email(request.getEmail())
.nickName(request.getNickName())
.name(request.getName())
.profileUrl(request.getProfileUrl())
.roleType(RoleType.PORT)
.password(request.getPassword())
.loginId(request.getLoginId())
.urlName(urlName)
.build();
newAdmin = adminRepository.save(newAdmin);
return AdminInfoResponse.from(newAdmin);
}
...
}
이때, 위의 `create()` 메소드에 관한 테스트 코드를 작성하던 중 일반적인 방법으로는 `Mocking` 할 수가 없었다.
처음에는 뭣도 모르고 아래처럼 그냥 `Stubbing`하면 되겠거니 하고 아래와 같이 코드를 작성했다.
그랬더니 아래와 같은 오류가 발생했다.
당연하게도 `RandomUtils`는 현재 `Mock` 객체가 아니므로 위와 같은 오류가 발생했다.
그래서, `RandomUtils`를 `Mock` 객체로 등록한 후 아래처럼 변수로 접근하면
당연하게도 static method 이여서 문제가된다.
그래서 이것저것 찾아 본 결과 `MockedStatic`이란 것을 알게 되었다.
`MockedStatic`은 static 메서드를 모킹하게 도와주는 클래스라고 생각하면 된다.
MockedStatic을 사용해야만 하는 이유가 뭘까?
우선, `Java`에서 그러한 듯 `static` 메서드는 클래스의 인스턴스가 없이 직접 호출된다.
즉, 클래스가 갖고 있는 고유한 메서드로 인스턴스의 상태에 의존하지 않는다.
따라서, `static method`는 일반적인 `Mockito`의 `@Mock`이나 `Mocktio.when()`을 사용한
모킹 기법으로는 모킹할 수 없다.
- `Java`의 `Reflection` 기술을 사용해서 동적 바인딩 시점에 `Mock` 객체를 만듬
- `Mockito`가 인스턴스 메서드에 대해서만 동작하도록 설계 됨.
원래는 `MockedStatic`이 없었으나 `Mockito 3.4.0` 버전부터 도입되었다고 한다.
그래서 우리는 MockedStatic을 활용하여 정적 메서드의 모킹 작업을 해야한다.
`MockedStatic`은 정적 메스드가 테스트되는 동안에만 모킹이 적용되도록 한다.
- 마찬가지로, `Reflection`을 사용해 `static` 메서드를 가로채 동작을 변경시켜 특정 모킹 작업이 일어나도록 한다.
`MockedStatic`의 실제 사용 코드
@Test
public void create_테스트() throws Exception{
//given
given(adminRepository
.existsByEmailAndLoginIdAndNickName(anyString(), anyString(), anyString()))
.willReturn(false);
given(adminRepository.save(any(Admin.class))).willReturn(admin);
MockedStatic<RandomUtils> mockedStatic = mockStatic(RandomUtils.class);
mockedStatic.when(RandomUtils::generateAlphaNumericRandomCode).thenReturn("test");
//when
AdminInfoResponse response = adminCommandService.create(createRequest);
//then
assertNotNull(response);
assertEquals(1L, response.getAdminId());
assertEquals(admin.getName(), response.getName());
assertEquals(admin.getNickName(), response.getNickName());
assertEquals("test", response.getUrlName());
assertEquals(admin.getEmail(), response.getEmail());
assertEquals(admin.getProfileUrl(), response.getProfileUrl());
assertEquals("test", response.getUrlName());
verify(passwordEncoder, times(1)).encode(anyString());
mockedStatic.close();
}
`MockedStatic<RandomUtils> mockedStatic = mockStatic(RandomUtils.class)`에서 볼 수 있듯이
모킹하고자 하는 클래스를 `mockStatic()` 메서드를 활용해 `MockedStatic`에 주입한다.
이렇게 했을 때 해당 `MockedStatic`를 활용해 `mockedStatic.when(...)`에서 원래 하던 방식처럼
메서드의 작업을 `Stubbing` 할 수 있게 된다.
이때, 중요한 점은 꼭 맨 마지막 코드처럼 `mockedStatic.close()`를 해줘야 한다.
close()를 해줘야 하는 이유
- 전역적인 상태 변경
- `MockedStatic`을 사용하면 `static` 메서드의 동작을 변경하는데, `static` 메서드는 클래스 레벨에서 전역적으로 호출되므로, 이를 모킹하면 해당 클래스가 전역적으로 영향을 받게 된다.
- 예를 들어, 어떤 테스트에서 `static` 메서드를 모킹하고, 테스트가 끝난 후에도 모킹된 상태가 유지되면, 이후의 테스트나 코드에서 예상치 못한 동작을 유발할 수 있다.
- 테스트 독립성 유지
- 테스트는 독립적이어야 한다.
- 따라서, 하나의 테스트에서 사용한 모킹 설정이 다른 테스트에 영향을 미치면 안된다.
- `MockedStatic`을 닫지 않으면, 그 모킹 설정이 다음 테스트에 영향을 미칠 수 있다.(필자의 경우 오류가 발생했다.)
- 자원 해제
- `MockedStatic`은 리소스를 사용하기 때문에, 테스트가 끝난 후에 이를 명시적으로 해제해야 한다.
- 리소스를 해제하지 않으면 메모리 누수나 불필요한 자원 점유가 발생할 수 있다고 한다.
- JVM의 동적 클래싱과의 상호작용
- `MockedStatic`은 내부적으로 JVM의 동적 클래싱 기능을 활용해 `static` 메소드를 가로챈다.
- 따라서, 테스트가 끝난 후 상태를 원래대로 되돌리지 않으면 JVM 내부적으로 상태가 꼬일 수 있다고 한다.
그렇다면 `static method`에서 무조건 `MockedStatic` 사용이 좋을까?
자료를 찾다보니 그렇지는 않다.
`MockedStatic`을 사용하면 테스트 속도에 저하가 있다고 한다.
테스트 속도 저하
- `MockedStatic`은 정적 메서드의 동작을 변경한다.
- 이 과정에서 자원을 할당하고 해제하는 작업이 발생
- `mockStatic()` 호출 시 정적 메서드를 트래킹하고, 메서드가 호출될 때마다 해당 메서드를 모킹해야 하므로 성능에 일정한 영향을 미친다.
메서드 호출 가로채기
- `MockedStatic`은 정적 메서드 호출을 가로채서 해당 메서드가 어떻게 동작할지를 임시로 변경한다.
- 이 가로채는 과정에서 미리 정의된 동작을 반환하도록 설정하는데, 이 과정 자체가 추가적인 연산이다.
- 이는 메서드 호출이 발생할 때마다 실행되므로, 성능에 영향이 있을 수 있다.
즉, 정리하자면 해당 정적 메서드의 호출이 많으면 그만큼 성능 저하가 두드러질 수 있다고 한다.
'Spring > Test' 카테고리의 다른 글
[테스트 코드] 스프링에서의 Redis 테스트 환경 구축 (0) | 2024.11.29 |
---|---|
[테스트 코드] @DataJpaTest 사용 (0) | 2024.11.24 |
[테스트 코드] 단위 테스트 @InjectMock 사용방법 (0) | 2024.11.24 |
[테스트 코드] 테스트 코드 이해하기 (0) | 2024.11.22 |
[테스트 코드] MockMvc, MockBean (0) | 2024.11.21 |