Spring Boot(2) Mockito를 이용한 단위 테스트
개발 환경 : JAVA 1.8 / Spring Boot 2.4.1 / Gradle 6.7.1 / MySql
IDE : IntelliJ 20.3.3
Dependency : spring-boot-starter-test:2.4.1 or mockito-all:1.10.19
저번에 이어 이번엔 Mockito라는 SpringBoot Test에 자주 사용되는 라이브러리를 가지고 단위 테스트를 공부하고,
공부하며 알게된 것들을 정리하고 저번처럼 예시를 가지고 실습도 해보려한다.
1. Mockito란? ( aka. Test Double )
Mockito 란 Mock을 지원하는 프레임워크로, 보통 Spring Boot Test에서 사용하는 JUnit 위에서 동작하며
Mock객체를 만들고 관리하고 검증할 수 있는 방법을 제공해주는 라이브러리이다.
구현체가 아직 없는 경우나, 구현체가 있더라도 특정 단위 테스트를 하고 싶을 경우 주로 사용한다고 한다.
즉, 단위 테스트를 공부하는 나에게 있어 아주 좋은 라이브러리란 말씀
1.1 Test Double ?
Mockito를 왜 사용하는지 이해하고, 또 잘 사용하려면 Test Double이란 것에 대해 알 필요가 있었다.
알기 싫어도 Mockito에 관한 글 몇가지만 찾아봐도 Test Double 얘기가 십중팔구 나오니 피할 수 가 없다.
Test Double이 뭔고하니,
쉽게 말해 테스트할 때 실제 객체의 역할을 대신 해주는 객체를 의미한다.
Test Double의 어원 자체도 영화 촬영 시 위험한 장면을 배우 대신 해주는 " 스턴트 더블" 이란 말에서 따왔다고 하니 이해하기 좋았다.
" 예를 들면, 우리가 DB로부터 조회한 값을 연산하는 로직을 구현했다고 하면
해당 로직을 테스트하기 위해선 항상 DB의 영향을 받을 것이고,
이는 DB의 상태에 따라 다른 결과를 유발할 수도 있다.
이렇게 테스트하려는 객체와 연관된 객체를 사용하기 어렵고 모호할 때 대신 해주는 객체를 Test Double이라고 한다. "
- 스티치 - Test Double을 알아보자 / 우아한 테크 코스 2020. 10. 19
Test Double은 종류도 여러개이고 종류별로 내용도 차이가 많으므로
자세한 사항은 위 링크를 확인하자.
이번 연습에서는 테스트 시 가장 많이 사용할 것 같은 Stub과 Mock 에 관해서만 먼저 간단히 알아보자.
Stub
로직은 없고 단지 원하는 값을 반환하는 Test Double로 실제로 동작하는 것 '처럼' 만들어 놓은 객체이다.
인터페이스 또는 기본 클래스가 최소한으로 구현된 상태이며, 테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 제공한다.
정리하자면
[인터페이스(기본 클래스)을 구현하며 미리 결과를 준비 -> 테스트 대상인 메서드 등 호출 -> 준비된 결과와 같은 상태가 되었는지 검증]
이런 과정을 거치는 구조이다. 따라서 Stub를 활용한 테스트 방식을 "상태기반 테스트 방식" 이라고 한다.
Mock
Mock은 테스트하고자 하는 코드와 맞물려 동작하는 객체들을 대신하여 동작하기 위해 만들어지는 껍데기 객체이다.
테스트 대상 Mock객체는 내부의 가상화된 메소드(객체)를 부를 수 있고,
가상화된 메소드(객체)가 호출에 따라 어떤 결과를 반환할 지 결정하고
해당 내용에 따라 동작하도록 프로그래밍된 객체가 바로 Mock 객체이다.
이렇게 Mock은 객체 사이의 행위(관계, Interaction)를 테스트하기 위해 사용한다.
이렇게 글로 읽고 이해하려하니 영 머리가 아파온다.
실습을 통해 확인해보자.
2. 필요한 Dependency 추가
Mockito를 사용하기 위해선 Dependency를 추가해줘야 한다.
하지만 SpringBoot를 사용하면 Gradle에 spring-boot-starter-test를 추가하게 되는데,
그 안에 mockito-core, mockito-junit-jupiter 패키지가 포함되어있다.
만약 SpringBoot를 사용하지 않거나 사용하더라도 Mockito 라이브러리가 잡히지 않는다면 임의로 추가해주자
// Gradle의 경우
// https://mvnrepository.com/artifact/org.mockito/mockito-all
testImplementation group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
// maven의 경우
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
3. 테스트 클래스 작성 및 테스트
3.1 Mapper 테스트를 위한 클래스 작성 및 테스트
orderMapper.java
...
@Mapper
public interface OrderMapper {
OrderVo getOrder(Long seq);
List<OrderVO> getOrders();
}
orderService.java
...
public class OrderService {
private final OrderMapper orderMapper;
public OrderVO getOrder(Long seq) {
return orderMapper.getOrder(seq);
}
public List<OrderVO> getOrders() {
return orderMapper.getOrders();
}
...
}
orderVO.java
...
@Getter
@Setter
@ToString
public class OrderVO extends MemberPayExit{
public OrderVO() {
}
// test를 위한 생성자
public OrderVO(Long seq, String productName) {
setSeq(1L);
this.productSeq = seq;
this.productName = productName;
}
private Long productseq;
private String productName;
...
}
orderTest.java ( mapper 테스트 )
...
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import static org.assertj.core.api.Assertions.assertThat;
// BDDMockito는 아래서 추가 설명
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
...
@Slf4j
public class OrderTest {
@Mock
private OrderMapper orderMapper;
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
@DisplayName("Order 매퍼 테스트")
public void mapper_테스트_Mockito() {
// given
// given에 왜 when...?
when(orderMapper.getOrder(1L)).thenReturn(new OrderVO(1L, "test"));
// when
OrderVO getOrder = orderMapper.getOrder(1L);
// then
verify(orderMapper).getOrder(1L);
assertThan(getOrder.getSeq()).isEqualTo(1L);
assertThat(getOrder.getProductSeq()).isEqualTo(1L);
assertThat(getOrder.getProductName()).isEqualTo("test");
// log
log.info("getOrder : " + getOrder.getSeq() + ", " + getOrder.getProductSeq() + ", " + getOrder.getProductName());
}
}
테스트 대상 인터페이스인 orderMapper를 선언하고 @Mock 어노테이션을 달아준다.
이를 통해 Mockito에선 해당 인터페이스의 껍데기 Mock객체를 생성해주게 된다.
이후 @BeforeEach ( Junit4에선 @Before ) 어노테이션 아래 MockitoAnnotations.initMocks(this) 선언을 통해 해당 테스트 환경에서 Mockito 어노테이션들을 읽어오도록 한다.
테스트 메서드 내부에선
when() 메서드를 사용하여 가짜 객체 orderMapper의 테스트 대상 메서드가 어떤 결과를 리턴할 지 지정해준다. ( // given )
그 후 해당 메서드를 수행하도록 하고 ( // when ),
verify() 메서드를 사용하여 가짜 객체가 정말 해당 메서드를 수행했는지,
수행한 기능이 위에 지정한 결과와 동일한 지를 비교하여 테스트 결과를 도출한다. ( // then )
테스트 결과
3.2 Service 테스트
orderTest.java 변경 및 추가
...
public class OrderTest {
@Mock
private OrderMapper orderMapper;
@Spy
@InjectMocks
private OrderService orderService;
...
@Test
@DisplayName("Order 서비스 테스트")
public void service_테스트_Mockito {
// given
when(orderMapper.getOrder(1L)).thenReturn(new OrderVO(1L, "test"));
// when
OrderVO vo = orderService.getOrderTest(1L);
// then
verify(orderMapper).getOrder(1L); // Mapper 매소드 실행 확인
verify(orderService).getOrder(1L); // Service 메서드 실행 확인
assertThat(vo.getSeq()).isEqualTo(1L);
assertThat(vo.getProductSeq()).isEqualTo(1L);
assertThat(vo.getProductName()).isEqualTo("test");
...
}
}
mapper 테스트에서 변경된 점은 OrderService를 선언할 때 @Spy와 @InjectMocks 라는 어노테이션을 추가한 것이 가장 눈에 띌 것이다.
Spy는 위에서 언급한 Test Double 중 한 종류로 Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록한다.
따라서 완전한 껍데기인 Mock과 달리 실제 객체처럼 동작시킬 수 있고, 필요한 부분에서만 Stub로 만들어서 동작을 지정할 수도 있다.
OrderService의 getOrder 메서드 내에서 OrderMapper의 getOrder가 선언되어 있기때문에 해당 정보를 기록하도록 하고,
또 생성자로 생성된 객체의 경우 verify를 통해 실행 여부를 확인할 수 없기때문에 @Spy를 통해 사용할 수 있는 상태로 만들어주기도 한다.
InjectMocks는 @InjectMocks가 붙은 객체를 생성하고 생성된 객체에 @Mock이 붙은 객체를 주입 시킬 수 있는 어노테이션이다.
흔히 사용하는 MVC패턴에서는 @InjectMocks(Service) / @Mock(repositories or Mappers ) / @Mock(DAOs)
등의 형태로 사용하게 된다.
테스트 결과
3.3 BDDMockito ??
Mockito를 통한 단위 테스트 연습을 하는 동안 정말 헷갈렸던 부분이 있었는데
Test Double 등의 개념은 물론이지만 특히나 날 헷갈리게 만들었던 부분은 바로
when() 이 녀석이다.
예제들을 보면서
'아니 when()으로 써놓고 왜 given에서 쓰는거지? 그럼 아래 when은 뭐고? '
보통 테스트를 할 때 가장 보편적으로 사용하는 방법인 given/when/then의 차례가 무너지면서 엄청난 혼란에 빠졌었다.
그런 상황을 나만 겪은게 아닌건지
Mockito에서 BDDMockito라는 패키지를 추가했는데,
알아보니 기능적으로는 별 차이가 없고 그저 given/when/then 을 잘 지키면서 쓸 수 있도록 틀을 바꿔주는 라이브러리란다.
BDDMockito에서 BDD(Behavior-Driven Development, 행위 주도 개발)는 또 다음에 공부해야 할 목록으로 넣어두고,
직접 써보니 너무 알아보기 편해서 좋아졌다 ㅎ
BDDMockito를 사용하여 리팩토링한 코드로 직접 보자.
...
@Test
@DisplayName("Order 서비스 테스트")
public void service_테스트_Mockito() throws Exception {
// given
given(orderMapper.getOrder(1L)).willReturn(new OrderVO(1L, "test"));
// when
OrderVO vo = orderService.getOrderTest(1L);
// then
then(orderMapper).should().getOrder(1L);
then(orderService).should().getOrderTest(1L);
assertThat(vo.getSeq()).isEqualTo(1L);
assertThat(vo.getProductSeq()).isEqualTo(1L);
assertThat(vo.getProductName()).isEqualTo("test");
// log
log.info(vo.toString());
}
...
짜잔!
알아보기 너무 편해진 코드다...
심지어 verify도 then()의 형태로 바꿔주고, verify에서 사용 가능했던 atLeastOne() 등의 메서드는 그대로 사용할 수 있다!
감사합니다 선생님들...
4. 정리
이렇게 두번째 테스트 공부가 끝났다.
테스트 주도 개발이 중요하다, 정말 좋다 란 말은 많이 들어왔지만
막상 테스트 코드를 작성하는데 시간을 더 잡아먹고 애초에 제대로 짤 줄도 몰라서 짜봐야 항상 전체테스트를 돌려버려서
시간을 너무 잡아먹었었는데,
이번 공부를 계기로 테스트 작성법에 대해서 어느정도 공부도 됐고,
내가 개발하는 중에 이런 테스트를 미리 짜고서 검증해가면서 개발했다면 삽질도 많이 줄었을 것 같다는 생각이 들었다.
일단은 내 토이 프로젝트를 진행하면서 테스트 주도 개발에 대해서 좀 더 익숙해진 후,
회사에서 새로운 프로젝트에 투입된다면 한번 실무에서도 도전해보고싶다!
* 참고자료
Mockito – Difference between @Mock and @InjectMocks annotations
Java - Mockito의 @Mock, @Spy, @Captor, @InjectMocks
mockito를 활용한 Service, Mapper(혹은 DAO) 테스트
Mockito @Mock @MockBean @Spy @SpyBean 차이점 정리
데이터 사용 Service를 mockito로 테스트하기