TIL - Service 코드 Mockito로 테스트 (Junit4 / Junit5)

su·2023년 8월 2일
0

TIL

목록 보기
56/93
post-thumbnail

문제 - Test 코드 실행에서 NullPointerException

Service에 대한 Test 코드 작성하기

Controller - Service - Repository로 구성되어 있는 코드에서, Service를 테스트 하고자 한다.
다만 Service에서는 Repository에서 데이터를 찾거나, 저장하거나, 삭제하는 로직이 있다.
실제 DB에는 영향을 미치지 않게 하기 위해서 가짜 객체인 @Mock으로 Repository를 선언하여 테스트 코드를 작성하고자 했다.

@ExtendWith(MockitoExtension.class)  // Mockito를 사용하기 위한 에너테이션 (Junit5)
public ServiceTest() {
	
    @InjectMocks
    PostService postService;
    
    @Mock
    PostRepository postRepository;
    
    @BeforeEach
    void init() {
    	// Junit5에서 Mockito를 사용할 때, Mockito의 에너테이션을 초기화하는 방법 중 하나
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    void createPostTest() {
    	// given
		User user = new User();
		PostRequestDto requestDto = new PostRequestDto("title", "contents");
        // 새 Post를 만듦
		Post savedPost = new Post(requestDto, user);
		savedPost.setId(20L);

		// when
        // postRepository에서 save 메소드를 실행하면 savedPost를 return 하도록 설정
		when(postRepository.save(any(Post.class))).thenReturn(savedPost);
		PostResponseDto result = postService.createPost(requestDto, user);

		// then
        // test에서 만들어 둔 savedPost의 제목/내용과 가져온 result의 제목/내용이 일치한지 확인
		assertEquals(savedPost.getTitle(), result.getTitle());
		assertEquals(savedPost.getContents(), result.getContents());
    }
	
}

1) 문제

그리고 테스트 코드를 실행한 결과, 오류가 발생했다 .. 🥲

Cannot invoke "com.example.post3.entity.Post.getId()" because "post" is null
java.lang.NullPointerException:
Cannot invoke "com.example.post3.entity.Post.getId()" because "post" is null
...

오류 메세지 내용은 post가 null이기 때문에 getId()를 할 수 없다는 것이었다.
왜지 ..? 나는 분명 테스트 코드에서 repository에서 save를 하면,
테스트 코드에서 정의해둔 savedPost를 return 하도록 코드를 작성해두었는데 ..

when(postRepository.save(any(Post.class))).thenReturn(savedPost);

어디서 문제가 생긴건지 도무지 알 수가 없다..
when().thenReturn() 여기서 문제가 생긴 건 아닌 듯 하다.
디버깅을 해본 결과, 해당 when().thenReturn() 절에는 원하는 대로 값이 들어갔다.

단, 실제 Serivce 코드 안으로 들어가면 post에 내가 설정해둔 값이 들어가지 않았다.

2) 시도 - Junit4로 바꿔서 테스트 실행

Junit의 버전을 바꿔서 테스트 코드를 작성해보기로 했다.
그러기 위해서는 Junit4를 gradle에 추가해주어야 한다! (원래는 Junit5)
(사실 오류가 떠서 보니, Junit4를 gradle에 추가하라고 했다..ㅎ)

위에서는 게시글 작성으로 테스트를 진행했고,
아래 부터는 repository에서 findById를 통해서 게시글 한 건을 조회해오는 테스트를 진행했다!

@RunWith(MockitoJUnitRunner.class)                // Mockito를 사용하기 위한 에너테이션 (Junit4)
@TestInstance(TestInstance.Lifecycle.PER_METHOD)  
public ServiceTest() {
	
    @InjectMocks
    PostService postService;
    
    @Mock
    PostRepository postRepository;
    
    @BeforeEach
    void init() {
    	// Junit5에서 Mockito를 사용할 때, Mockito의 에너테이션을 초기화하는 방법 중 하나
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    void getOnePostTest() {
    	// given
    	User user = new User();
		PostRequestDto postRequestDto = new PostRequestDto("title", "contents");
		var newPost = Post.builder().requestDto(postRequestDto).user(user).build();

		// when
    	given(postRepository.findById(20L)).willReturn(Optional.ofNullable(newPost));
		PostResponseDto postResponseDto = postService.getOnePost(20L);

		// then
		then(newPost.getTitle()).equals(postResponseDto.getTitle());
		then(newPost.getContents()).equals(postResponseDto.getContents());
    }
	
}

이렇게 작성해주었다.
위에서 when().thenResult() 으로 조건을 주었던 부분을 given().willReturn() 으로 수정했다.
원하는 대로 코드가 동작했고, 테스트 코드도 통과했다 !

3) 해결 - Junit5로 테스트 실행

Junit4에서 테스트 코드가 실행되었으니, 다시 Junit5로 돌아와서 진행해보려고 한다 (이악물)

어디서 막히는지 알 수가 없어서 디버깅을 몇 번이나 시도해봤다.
우선 Test 코드에서는 문제가 되는 부분이 없었다.
newPost를 생성하는 부분도 그렇고, when().thenResult()로 조건을 주는 부분에도 그렇고,
newPost가 동일하게 들어가는 것을 확인할 수 있었다.

저렇게 해뒀는데 왜 자꾸 실제 Service 코드에서는 값이 들어가지 않고 Exception이 발생하는지..

에 대한 정답을 찾았다 !!! .. 허무하다
Test 코드에서 @Mock으로 설정해둔 repository들을, service에서 제대로 받아가지 못한 것 같다.
그래서 자꾸 service에서 실행을 하면, @Mock의 repository가 아닌, 실제 repository를 갖고 코드를 진행하기 때문에, 내가 작성해둔 테스트 코드가 먹히지 않았던 것이다..!

PostServiceImpl postService = new PostServiceImpl(postRepository);

를 위에 선언해두고 테스트 코드를 실행하면 된다 !

참고

4) 배운 점

when().thenReturn() / given().willReturn()

처음에는 Junit 버전에 따라서 다르게 사용하는 거라 오류가 나나? 싶었는데 그건 아니었다.

when().thenReturn()

  • org.mockito.Mockito.when를 import 해서 사용한다.
  • 메서드 호출에 대한 동작을 지정하고 싶을 때 사용한다.
  • BDD 스타일 보다, 전통적인 테스트 스타일을 선호하는 경우 주로 사용된다.
  • 특정 호출에 대한 결과를 설정한다.

given().willReturn()

  • org.mockito.BDDMockito.then를 import 해서 사용한다.
  • BDD(행위 주도 개발) 스타일의 Mockito 문법을 이용하는 데에 사용된다.
    - BDD: 테스트 코드를 보다 자연스럽고 가독성 있게 작성할 수 있도록 돕는다.
  • 주어진 상황에서 Mock 객체의 메서드 호출에 대한 행위를 지정하는 데 사용된다.
    • 테스트의 전체 흐름을 더 명확하게 표현할 수 있다.

예시 코드로 비교해보자면,

@Mock
PostRepository postRepository;

var newPost = Post.builder().title(title).content(content).build();

// when 사용
when(postRepository.save(any())).thenReturn(newPost)

// given 사용
given(postRepository.save(any())).willReturn(newPost);

이렇게 사용할 수 있다!

⁕ @InjectMocks이 Junit4에서는 되는데, Junit5에서는 안된다?

@InjectMocks라는 에너테이션이 있어서 Service를 테스트하고자,
내가 테스트 하려는 PostService 필드 위에 달아주었다.
@InjectMocks: @Spy, @Mock 에너테이션이 붙은 객체들을 주입시켜준다.

Junit4 테스트 진행에서는 문제 없이 객체를 주입받아 사용되었는데,
Junit5에서는 저 에너테이션이 제대로 동작하지 않아서 테스트가 계속 실패했던 것이다.

⁕ Test 코드 작성 시 Junit4 / Junit5에서 Mockito 사용의 에너테이션의 차이

Test 파일 위에
Junit4 에서는 @RunWith(MockitoJunitRunner.class) 을 달아주고
Junit5 에서는 @ExtendWith(MockitoExtension.class) 을 달아준다.

만약 해당 에너테이션을 달아주지 않으면,
아래와 같은 메서드를 만들어 주어야 한다.

// Junit4
@BeforeEach
void init_junit4() {
	MockitoAnnotations.initMocks(this);
}

// Junit5
@BeforeEach
void init_junit5() {
	MockitoAnnotations.openMocks(this);
}

참고

이틀 동안 고민하던 문제를 해결할 수 있어서 다행이라고 생각한다.
무엇보다 테스트 코드를 원하는 대로 짜보는 게 처음이었는데,
어느정도 감이 잡힌 듯 하다 !
다른 테스트 코드도 작성해보면 더 익숙해질 수 있을 것 같다.

profile
(❁´◡`❁)

0개의 댓글