프로젝트/뉴스피드

[단위 테스트/Mockito] 게시글 서비스레이어 단위 테스트 - 2 (with JUnit 버전 문제)

writtenbyrla 2024. 2. 26. 21:31

단위 테스트 이전 글

 

[단위 테스트/Mockito] 게시글 서비스 레이어 단위 테스트 - 1 (with ReflectionTestUtils)

MockMvc를 이용하여 컨트롤러 통합테스트를 끝냈다. 테스트를 위한 별도 DB를 생성하여 모든 예외사항에 대해 전체 테스트를 끝냈는데, 아무리 생각해도 비즈니스 로직은 서비스단에 포함되어 있

writtenbyrla.tistory.com

 

 


 

 

 

 

Mockito를 사용해서 서비스 레이어 단위 테스트를 열심히 하다가 또 한 번 위기를 겪었다.

Mockito에서 Mock 객체를 사용하기 위해서 테스트 파일에 어노테이션을 달아주는데, 나는 계속해서 JUnit4 방식인 @RunWith(MockitoJUnitRunner.class) 어노테이션을 달고 있었고, JUnit5 방식은 @ExtendWith(MockitoExtension.class)로 바뀌었다고 한다.

 

 

차이점

@RunWith(MockitoJUnitRunner.class)

  • JUnit 4에서 사용
  • Mockito가 JUnit 테스트를 실행하는 데 사용되는 테스트 러너(runner)를 지정
  • MockitoJUnitRunner는 Mockito 테스트를 실행하기 위해 내장된 JUnit 테스트 러너로, Mockito가 Mock 객체를 초기화하고 관리하는 데 필요한 작업을 수행
  • MockitoJUnitRunner를 사용하면 별도의 Mockito 초기화 코드를 작성하지 않아도 됨

 

@ExtendWith(MockitoExtension.class):

  • JUnit 5에서 사용
  • JUnit Jupiter 확장(extension)을 지정
  • MockitoExtension은 Mockito가 JUnit 5 테스트를 실행하는 데 필요한 확장으로, Mockito와 JUnit 5를 통합하는 데 사용
  • MockitoExtension을 사용하면 JUnit 5의 테스트 실행 수명주기에 Mockito 초기화와 관련된 작업을 수행할 수 있음
  • JUnit 5의 @ExtendWith 애노테이션은 다중 확장을 지원하기 때문에 다른 JUnit Jupiter 확장과 함께 사용할 수 있음

 

 

 

문제 상황


내 프로젝트의 JUnit 버전에 맞춰 어노테이션만 변경하였으나 RunWith로 통과하던 테스트가 일부 실패하기 시작했다.

살펴보니 조건문을 확인하거나 예외처리하는 부분, 목록 받아오는 부분에서 오류가 났다.

 

 

이 오류는 Mockito에서 불필요한 스텁(Unnecessary Stubbing)을 감지했을 때 발생하는 것으로, 불필요한 스텁이란 테스트 중에 호출되지 않는 모의 객체의 메서드에 대한 스텁을 의미한다. 한마디로 테스트 코드를 더 깔끔하고 유지보수 가능하게 만들기 위해서는 이 불필요한 코드를 제거해야 한다.

이전에는 불필요한 스텁이 있어도 로직에 문제가 없으면 테스트가 통과했지만, 바뀐 버전에서는 불필요한 요소가 테스트에 영향을 미칠 수 있으므로 테스트를 통과하지 못하게 하는 것이다.

 

예를 들면

게시글 등록 시 서비스단에서 유저 정보 확인 후 게시글의 제목이나 내용 중 하나의 입력값이 null일 때 PostException을 던지고 있어 나의 테스트 코드에서 유저 확인, 게시글 확인을 차례대로 하고 있었다. 계속 불필요한 스텁이 감지되었다는 오류가 떴고, 내가 이해한 바로는 게시글 작성 시 입력값이 null값일때 PostException을 던지는 경우를 테스트하고 싶으면 유저 확인에 대한 조건 없이 게시글 정보를 확인하는 조건만 테스트에서 걸어야 한다는 것 같았다.

 

 

해결 시도


1차 시도

JUnit5에서는 기존보다 테스트를 더 엄격하게 한다고 볼 수 있다. 이를 해결 하기 위해서 Mockito의 스텁의 엄격성을 lenient로 변경하면 불필요한 스텁에 대한 경고가 표시되지 않는다. 하지만 leneint를 사용하게 되면 임의로 허용되지 않은 것을 허용되게 해서 테스트를 통과하게 하는 것이므로 내가 정말 테스트하고자 하는 것에 영향을 끼칠 수 있다.

 

기존 코드

// 사용자가 없는 상황을 가정하여 예외 처리
given(userRepository.findById(100L)).willReturn(Optional.empty());

// then
assertThrows(UserException.class, () -> {
	postServiceImpl.createPost(post);
}, "등록된 사용자가 없습니다.");

 

수정 코드

// 사용자가 없는 상황을 가정하여 예외 처리
lenient().when(userRepository.findById(100L)).thenReturn(Optional.empty());

// then
assertThrows(UserException.class, () -> {
	postServiceImpl.createPost(post);
}, "등록된 사용자가 없습니다.");

 

 

 

 

2차 시도

근본적인 해결을 위해서는 불필요한 스텁에 대한 제거가 필요하다. lenient을 표시하여 임의로 조정하지 말고, 테스트 시 주어지는 when 조건에 필요한 것만 넣어서 테스트하는 방식을 택해야 할 것 같다.

given에서 필요한 데이터 생성시 필요한 필드를 꼼꼼하게 채워주고 when에서는 테스트 하고자 하는 예외처리 상황에 대해서만 findbyId만 수행해 주면 된다.

예외처리가 필요한 곳에서만 when을 걸고 불필요한 stubbing은 제외해야 하는것이 핵심이다.

 

@DisplayName("게시글 작성 실패 - 유저 정보 없음")
@Test
void create_post_fail_not_found_user() {
	CreatePostDto post = CreatePostDto.builder().title("제목").content("내용").userId(100L).build();

	// 사용자가 없는 상황을 가정하여 예외 처리
	when(userRepository.findById(post.getUserId())).thenReturn(Optional.empty());

	assertThrows(UserException.class, () -> {
		postServiceImpl.createPost(post);
		}, "등록된 사용자가 없습니다.");
}
@DisplayName("게시글 작성 실패 - 내용 없음")
@Test
void create_post_fail_non_content() {
	User user = TestUtil.createUser(1L);
    var post = CreatePostDto.builder().title("제목").userId(user.getUserId()).build();

    assertThrows(PostException.class, () -> {
		postServiceImpl.createPost(post);
	},"내용을 입력해주세요.");
}

 

 


 

 

 

이렇게 테스트 코드를 다시 전체적으로 보면서 정말 테스트하고자 하는 부분만 남기고 불필요한 코드들은 지워가면서 리팩토링 하였더니 전부 다 통과할 수 있었다.

문제를 해결함에 있어 임시방편도 때로는 좋지만 문제상황을 없애서 해결할 수 있도록 고민하는 것이 중요하고, 그렇게 고민하다 보면 작동 원리에 대해 근본적으로 파고들어 생각해 보게 되는 것 같다!