Mockito를 활용한 단위테스트
최근에 테스트 함수를 작성하기 위해 Mockito라는 테스트 프레임워크를 사용한 일이 있었다. 그런데 결국 어려운 개념이 아니었음에도, Mockito를 사용해야하는 이유를 제대로 받아들이는데 꽤 오랜 시간이 걸렸다. 따라서 다른 사람들의 시행착오 시간을 줄여주기 위해 내가 Mockito를 사용해보며 이해한바를 공유하고자 한다.
Mockito란
먼저 Mockito는 Java 단위 테스트 프레임워크 중 하나로, 테스트를 Mock Object를 활용한다. 그렇다면 Mock Object란 무엇이란 말인가. 위키를 참조해보자.
In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.
주저리 주저리 써있긴 한데, behavior라는 단어에 집중하면 된다. Mock object는 일종의 가짜 객체로서 함수의 행위(behavior)를 테스트하는데 집중하기 위해 사용된다. 간단한 예시를 들어보자. 이 예시는 Yoo Young-mo님의 글에서 참조했다.(무척 좋은 글이다!)
위처럼 CellPhoneMmsSender를 구현하는 상황이라고 해보자. 그 과정에서 CellphoneService라는 객체의 sendMMS 함수를 호출한다. 여기서 내가 구현한 send 함수를 테스트 한다면, 적절한 파라미터로 sendMMS 함수를 호출하는지만 확인하면 된다. sendMMS가 제대로 구현되었는지는 중요하지 않기 때문이다. 테스트의 주된 관심사가 아니다!
그렇다면 굳이 테스트를 위해 CellphoneService 객체를 생성해서 사용할 필요가 없다. 적당히 sendMMS의 정상 리턴값을 던져주는 Fake CellphoneService를 사용하면 되는 것이다. 왜? CellphoneMmsSender의 behavior를 테스트하는데 집중하기 위해! 이것이 Mock Object를 사용하는 이유이다.
회의실 예약 서비스
설명을 위해 조금 더 리얼한 예시를 들어보자. 회의실 예약 시스템을 개발해야 하는 상황이다. 그렇다면 대충 필요한 기능이 몇 가지 있을 것이다. 우선 예약을 하는 기능, 전체 방 정보를 조회하는 기능 등등.. 시스템의 개략적인 구조는 이렇게 될것이다.
ReservationSerivce는 3개의 메소드를 가지고 있고, Repository는 DAO의 역할을 한다. 이 상황에서 Mock 객체를 활용해 retrieveRooms 메소드를 테스트해보고 싶다면 어떻게 테스트 코드를 작성해야 할까.
❙ Mockito의 활용
우리의 관심사는 retrieveRooms라는 메소드이다. 이 메소드가 제대로된 하위 메소드를 호출하여 원하는 결과를 보여주는지, 메소드의 Behavior를 테스트하는데 집중하면 된다. 그 과정에서 Repository가 DB에 연결되어 제대로 동작하는지까지 확인할 필요는 없다.
아, 참고로 이 예제는 Springboot & Gradle 개발 환경에서 작성되었음을 밝힌다.
❙ 전체 회의실 목록 조회
ReservationService의 retrieveRooms라는 메소드를 테스트해보자. 이 메소드는 전체 회의실 목록을 조회하는 매우 간단한 기능을 제공한다. 코드는 다음과 같다.
public List<Room> retrieveRooms() throws Exception {
List<Room> roomList = roomRepository.findAll();
return roomList;
}
그렇다면 호출 구조는 다음과 같을 것이다.
우리는 retrieveRooms 메소드를 테스트 해볼것이기 때문에 주된 관심사는 물론 reservationService이고, roomRepository는 관심사가 아니다. 그러므로 가짜 객체인 Mock 객체로 생성할 것이다.
❙ 객체 선언
위와 같이 객체를 선언해준다. 언급했듯 Repository 객체는 우리의 주된 관심사가 아니다. 때문에 가짜 객체로 선언해주어도 무방하고, @Mock 어노테이션을 달아주면 된다.
그 다음, 테스트 수행 전 실행되는 Before 단계에서 실제 ReservationService 객체를 생성해준다. ReservationService는 실제로 테스트를 수행할, 우리가 매우 관심있는 객체기 때문에 실제 객체를 생성해준다.
그러면 본격적인 테스트 코드를 작성해보자. 예시로 전체 회의실 정보를 조회하는 retrieveRooms라는 메소드를 테스트해보기로 한다.
테스트 코드의 관례대로 given, when, then 3개의 절로 작성하였다. 먼저, given에서는 Mock 객체의 동작을 미리 정의한다. 쉽게말해 답정너인 셈이다.
따라서 given절에서는 roomRepository 객체는 어차피 가짜 객체고, findAll 메소드가 제대로 호출되기만 한다면 무조건 미리 생성해둔 roomList 객체를 리턴할 것이라는 예상답안을 정해놓은 것이다.
그리고 when 절에서는 실제 테스트 대상이 되는 retrieve 메소드를 수행한다.
then절에서는 비로소 retrieveRooms 메소드가 제대로 roomRepository의 findAll 메소드를 호출했는지를 검증한다. 예상답안과 실제 답안을 비교해보면서 말이다. 테스트 메소드를 실행하고, 테스트가 성공하면 이렇게 프로그레스바가 뜬다.
왠지 기분이 좋아진다.
하지만 끝이 아니다. 우리는 Repository 객체를 Mock 객체로 선언해주었다. 이제 관심사를 Repository로 옮겨서, 실제 데이터들을 제대로 넣고 꺼내오는지를 테스트해주어야 한다. 다음 포스팅에서 JPA Repository를 테스트해보도록 하겠다.
참고
https://medium.com/@SlackBeck/mock-object%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-85159754b2ac