본문 바로가기
Test

서비스 로직 Test 작성하기 (feat. Mockito)

by wch_t 2024. 6. 10.

1. '단위 테스트'의 정의부터 짚고 넘어가자.

프로젝트 개발 과정에서 일반적으로 한 개의 클래스 또는 메소드 수준으로 기능을 검증하는 테스트이다.

주로 각 service, controller 별로 테스트 클래스를 만들어서 진행하는데, 이것이 바로 단위 테스트를 의미한다.

 

 


 

2. 그럼 테스트를 할 때, 정의한 도메인 객체들을 그대로 사용해도 되나?

기존에 정의한 도메인 객체를 그대로 사용할 수도 있지만, 비효율적이며 다음과 같은 문제점을 가지고 있다.

 

- 테스트하려는 객체가 다른 객체에 의존하는 경우, 모든 의존성을 준비하는데 복잡하고 시간이 오래 걸린다.

- DB 또는 네트워크를 사용하는 테스트에서 관련 기능들을 호출하는 작업에서 많은 비용이 든다.

- 테스트 중 에러가 발생하면, 현재 시스템 문제인지 외부 시스템 문제인지 구분하기 어렵다.

 

따라서 기존 객체를 모방(mocking)하여 독립적으로 테스트를 진행하는데,

Java에서 기능 테스트를 하기 위해 지원하는 테스트 프레임워크가 바로 `Mockito`이다.

 

 


 

3. @ExtendWith()는 무슨  기능을 할까?

`@ExtendWith` 어노테이션은 JUnit5에서 테스트 클래스에 대한 다양한 확장 기능을 제공하기 위해 사용되는 어노테이션이다.

 

따라서 `@ExtendWith(MockitoExtension.class)` 를 사용하여 

Mockito가 제공하는 기능을 JUnit 테스트 클래스에서 사용할 수 있게, 확장을 추가해주면 된다.

 

추가로 많이 사용되고 있는 확장 기능으로 `@ExtendWith(SpringExtension.class)`이 있다.

이는 Spring Framework 내에서 테스트를 진행하고자 할 때 사용된다.

 

 


 

4. @Mock, @InjectMocks의 개념과 각 어노테이션이 어느 상황에서 사용될까?

@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {

    @InjectMocks private ArticleService sut; // System Under Test: 테스트 작성할 때 많이 사용하는 네이밍
    @Mock private ArticleRepository articleRepository;

}

 

@Mock

: 실제 객체를 모방한 가짜 객체로, 모방한 객체의 동작에 대해서 테스트를 진행할 수 있다.

 

- 그 안에 메소드를 호출해서 사용하려면 반드시 스터빙(stubbing) 해야 한다.

- 만약 스터빙을 하지 않고, 그냥 호출한다면 primitive type은 0 / 참조형은 null을 반환한다.

 

 

@InjectMocks

: 테스트 대상 객체에, @Mock으로 생성된 객체를 주입한다.

 

위의 예시 코드에서 ArticleService 내에 들어가 보면, ArticleRepository 객체가 필요함을 알 수 있다.

이와 같이 @InjectMock(Service), @Mock(DAO) 와 같이, Service 테스트 Mock 객체에 DAO Mock 객체를 주입시켜 사용한다.

@Service
public class ArticleService {
    private final ArticleRepository articleRepository;
}

 

 

+. Stub, Stubbing

: 실제 객체를 대신하여, 미리 정의된 응답을 제공하는 데 중점을 두는 가짜 객체이다. (Stub)

Mock 객체의 메소드를 호출해도, 실제 객체의 메소드가 실행되지 않기 때문에 메소드의 호출에 대한 응답을 정의해야 한다. (Stubbing)

when(), thenReturn(), thenThrow() 등을 사용해, 리턴 값 또는 예외 발생을 정의할 수 있다.

 

아래 코드를 예시로 Stubbing을 설명해보면

userRepositoryMock이라는 UserRepository 클래스의 mock 객체가 생성하였고, 이 mock 객체의 findById 메소드가 1L이라는 인자로 호출될 때, 새로운 User 객체를 반환하도록 설정한 코드이다.

즉, 이 코드는 findById 메소드 호출과 그 응답을 Stubbing한 것이다.

@Test
public void testGetUserName() {
    // UserRepository를 모킹하고 stub을 설정한다.
    UserRepository userRepositoryMock = mock(UserRepository.class);
    when(userRepositoryMock.findById(1L)).thenReturn(new User(1L, "John Doe"));

    UserService userService = new UserService(userRepositoryMock);

    String userName = userService.getUserName(1L);

    assertEquals("John Doe", userName);
}

 

 


 

5. MockMvc와 관련 라이브러리가 무엇이 있을까?

1) MockMvcRequestBuilders

- HTTP 요청을 생성하는데 사용된다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.html

// HTTP Get 요청 경로를 생성한다.
MockMvcRequestBuilders.get("/")

 

 

2) MockMvcResultMatchers

- HTTP 응답을 검증하는데 사용된다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/result/MockMvcResultMatchers.html

// HTTP 응답 상태를 검증한다.
MockMvcResultMatchers.status().isOk()

 

 


 

6. 그럼 이제 서비스 로직 테스트를 직접 작성해보자!

 

이처럼 만들고자 하는 기능을 점검할 단위 테스트를 먼저 작성하고,
단위 테스트를 통과시킬 수 있을 정도의 서비스 로직의 코드를 작성하면 된다.

 

 

cf. 예제 코드를 참고하였습니다.
https://github.com/djkeh/fastcampus-project-board 

 

 

 

[예시 1.]

게시글을 검색하면, 게시글 리스트를 반환하는 articleService의 searchArticles 메소드를 테스트 하는 코드이다.

 

@DisplayName("게시글을 검색하면, 게시글 리스트를 반환한다.")
@Test
public void searchArticles() throws Exception {
    //given

    //when
    Page<ArticleDto> articles = articleService.searchArticles(SearchType.TITLE, "search keyword"); // 제목, 본문, ID, 닉네임, 해시태그

    //then
    assertThat(articles).isNotNull();
}

 

given : 초기 상황

- 특별한 준비 작업이 없다.

 

 

when: 행위, 이벤트

- searchArticles 메소드를 호출하는 데 있어, 검색과 관련된 인자로는 '검색 타입', '검색 키워드'로 정의할 수 있다.

- 또한 return 값을 Page<ArticleDto>로 받으면서 게시글에 따른 Paging과 정렬 처리도 가능하게끔 한다.

(이 때, ArticleDto와 SearchType의 정의가 필요하다.)

 

 

then: 기대 결과

- articles 객체가 null이 아님을 검증한다.

즉, 검색 결과가 존재함을 확인한다.

 


 

 

[예시 2.]

게시글 정보를 입력하면, 게시글을 생성하는 articleService의 saveArticle 메소드가 articleRepository의 save 메소드를 정확히 호출하는지 테스트 하는 코드이다.

@DisplayName("게시글 정보를 입력하면, 게시글을 생성한다.")
@Test
public void saveArticle() throws Exception {
    //given
    BDDMockito.given(articleRepository.save(ArgumentMatchers.any(Article.class))).willReturn(null);

    //when
    articleService.saveArticle(ArticleDto.of("spring 공부 방법", "내용", "#java", LocalDateTime.now(), "sun"));

    //then
    BDDMockito.then(articleRepository).should().save(ArgumentMatchers.any(Article.class)); // save를 1번 호출했는가?

    // 데이터베이스에 원하는 데이터가 들어갔는지 판단하는 테스트
    // persistence 레이어까지 내려가서 확인을 해야한다. → unit test에서 더 확장된 sociable test

}

 

given : 초기 상황

- articleRepository.save() 메소드의 호출 결과를 미리 정의된 결과인 null로 모의한다.

 

이 코드는 단지 테스트 환경을 준비하는데 사용되고, 실제로 어떠한 테스트 동작을 수행하지도 않는다. 

그럼 왜 필요할 지 생각해 볼 필요가 있는데 "메소드 return 값의 구조를 잡는 것"이라고 생각하면 된다.
예를 들어, articleRepository.save() 성공 시 null / 실패 시 error 구조로 잡을 수 있다.

 

 

when: 행위, 이벤트

- saveArticle 메소드를 호출하는 데 있어, 관련된 인자로는 '게시글 데이터'로 정의할 수 있다.

 

 

then: 기대 결과

.then(articleRepository)

: articleRepository에 대한 BDDMockito 검증 시작

 

.should()

: articleRepository 메소드 중 어떤 메소드가 호출되었는지 유무 검증

 

.save(...)

: articleRepository 메소드 중 save 메소드 호출

 

.save(ArgumentMatchers.any(Article.class))

: save 시, Article 클래스 타입의 객체와 매칭되어야 함

 

- 즉, articleRepository 메소드 중에서 save 메소드가 정확히 한 번 호출되었는지를 검증한다.

 


 

[예시 3.]

게시글 ID를 입력하면, 게시글을 삭제하는 articleService의 deleteArticle 메소드가 articleRepository의 delete 메소드를 정확히 호출하는지 테스트 하는 코드이다.

@DisplayName("게시글의 ID를 입력하면, 게시글을 삭제한다.")
@Test
public void deleteArticle() throws Exception {
    //given
    BDDMockito.willDoNothing().given(articleRepository).delete(ArgumentMatchers.any(Article.class));

    //when
    articleService.deleteArticle(1L);

    //then
    BDDMockito.then(articleRepository).should().delete(ArgumentMatchers.any(Article.class)); // save를 1번 호출했는가?

}

 

given : 초기 상황

- 저번 코드에서는 articleRepository.save() 메소드의 호출 결과를 미리 정의된 결과인 null로 모의하였다.

- 이번 코드에서는 articleRepository.delete() 메소드의 호출될 때 아무런 동작도 하지 않도록 모의하였다.

 

이 코드도 똑같이 "메소드 return 값의 구조를 잡는 것"이라고 생각하면 된다.

 

 

when: 행위, 이벤트

- deleteArticle 메소드를 호출하는 데 있어, 관련된 인자로는 '게시글 ID'로 정의할 수 있다.

 

 

then: 기대 결과

- saveTest와 똑같이, articleRepository 메소드 중에서 delete 메소드가 정확히 한 번 호출되었는지를 검증한다.

 

 


 

 

참고.

 

단위테스트와 Stub 개념

https://velog.io/@u-nij/JUnit5-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Extendwith

 

@Mock, @Spy, @InjectMocks

https://cornswrold.tistory.com/369

https://effortguy.tistory.com/142


MockMvc - MockMvcRequestBuilders, MockMvcResultHandlers, MockMvcResultMatchers 주요 메서드 정리

https://ktko.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81Spring-MockMvc-%ED%85%8C%EC%8A%A4%ED%8A%B8

 

'Test' 카테고리의 다른 글

Mockito가 있는데 왜 BDDMockito를 사용할까?  (0) 2024.06.10