
개발을 하면서 단위 테스트를 꼼꼼히 작성하려는 노력을 들이고 있다.
현업에서는 테스트가 통과되지 않으면 머지를 막아놓는 경우도 있다 하니 ,,
어느 정도 Given-When-Then 단계를 따라 JUnit과 Mockito, AsserJ를 활용한 테스트 코드 작성에는 익숙해졌는데 아직도 부족한 점이 많은 것 같아 새로 알게 된 개념을 기록해보겠다
개발하고 있는 서비스는 좌석 예약 서비스이고, 로그인 대신 DTO로 유저 정보와 좌석 번호를 담아 보내서 예약 또는 예약 취소하는 API 를 짜고 있다.
테스트하는 코드는 다음과 같다.
@Transactional
public Seat deleteReservation(DeleteReservationRequest deleteReservationRequest){
Seat seat = seatRepository.findBySeatNo(deleteReservationRequest.seatNo())
.orElseThrow(() -> new EntityNotFoundException("존재하지 않는 좌석입니다. 좌석번호: " + deleteReservationRequest.seatNo()));
User user = seat.getUser();
String nickName = deleteReservationRequest.nickname();
String userName = deleteReservationRequest.userName();
String password = deleteReservationRequest.password();
if(seat.getStatus().equals(true)) {
if (user.getNickName().equals(nickName)
&& user.getUserName().equals(userName)
&& user.getPassword().equals(password)) {
seat = seat.toBuilder()
.user(null)
.status(false)
.build();
user = user.toBuilder()
.hasReservation(false)
.seat(null)
.build();
seatRepository.save(seat);
userRepository.save(user);
}else {
throw new CustomException(ErrorCode.UNAUTHORIZED_USER, HttpStatus.BAD_REQUEST);
}
}else{
throw new CustomException(ErrorCode.UNVALID_DELETE_REQUEST, HttpStatus.BAD_REQUEST);
}
return seat;
}
흐름을 적어보자면 다음과 같다.
if 문으로 좌석이 예약된 상태인지 검증
request로 전달된 유저 정보와 좌석에 예약된 유저 정보 검증
유저 검증이 완료됐다면 좌석과 유저를 업데이트하고 DB에 저장
우선 가장 기본적인 단위인 삭제 검증에 대한 테스트 코드를 다음과 같이 작성했다.
@Test
@DisplayName("삭제 성공")
void deleteReservationSuccess() {
DeleteReservationRequest request = new DeleteReservationRequest("userName", "1111", "userNickname", 1L);
Seat seat = Seat.builder().seatNo(1L).user(User.builder().nickName("userNickname").userName("userName").hasReservation(true).password("1111").build()).status(true).build();
User user = seat.getUser();
when(mockSeatRepository.findBySeatNo(eq(1L))).thenReturn(Optional.of(seat));
// when
Seat deletedSeat = reservationService.deleteReservation(request);
// then
assertNull(deletedSeat.getUser());
assertFalse(deletedSeat.getStatus());
assertFalse(user.getHasReservation());
assertNull(user.getSeat());
verify(mockSeatRepository, times(1)).findBySeatNo(eq(1L));
verify(mockSeatRepository, times(1)).save(deletedSeat);
verify(mockUserRepository, times(1)).save(user);
}
아주 단순하고 아름다운 코드 +_+ .. 라 생각했지만
assertFalse(user.getHasReservation())에서 False가 아니라 실제 값이 True라고 걸렸다.
원인은 toBuilder와 관련이 있었다. toBuilder의 동작 구조를 살짝 살펴보자.
toBuilder
불변객체의 값을 변경할 때 사용하는 @Build에서 제공하는 속성이다.
A라는 객체가 있으면 이를 복사한 A` 를 생성하고 값을 변경해 빌드한뒤, A에 덮어씌운다.
User 객체를 toBuilder로 복사해 빌드한 뒤 save 메소드에 전달했기 때문에
빌드한 user 객체와 seat.getUser()로 받아온 user 객체는 엄연히 다른 객체다.
따라서 테스트 코드에서는 seat.getUser()로 초기화한 user 객체를 검증하고 있기 때문에 테스트가 통과되지 못한 것이다.
Mockito 프레임 워크에서 ArgumentCaptor 기능을 제공한다.
ArgumentCaptor는 메소드에 전달된 인자를 캡처하여 검증할 수 있도록 한다.
1. Captor 어노테이션을 사용해 캡처할 인자를 선언한다.
ArgumentCaptor<$T> argumentCaptor;
T는 가져오고 싶은 인자의 타입형
@Captor
private ArgumentCaptor<User> userCaptor;
2. capture() 메소드로 메소드에 전달된 인자를 캡처한다.
verify(userRepository).save(userCaptor.capture());
3. 캡처한 인자의 값을 가져와 검증한다.
User capturedUser = userCaptor.getValue();
그리하여 완성된 테스트 코드는 다음과 같다.
@Test
@DisplayName("삭제 성공")
void deleteReservationSuccess() {
// Given
DeleteReservationRequest request = new DeleteReservationRequest("userName", "1111", "userNickname", 1L);
User user = User.builder()
.nickName("userNickname")
.userName("userName")
.hasReservation(true)
.password("1111")
.build();
Seat seat = Seat.builder().seatNo(1L).user(user).status(true).build();
when(mockSeatRepository.findBySeatNo(eq(1L))).thenReturn(Optional.of(seat));
// When
Seat deletedSeat = reservationService.deleteReservation(request);
// Then
verify(mockUserRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertNull(deletedSeat.getUser());
assertFalse(deletedSeat.getStatus());
assertThat(capturedUser.getHasReservation()).isFalse();
assertNull(capturedUser.getSeat());
verify(mockSeatRepository, times(1)).findBySeatNo(eq(1L));
verify(mockSeatRepository, times(1)).save(deletedSeat);
}

이대로 끝내긴 아쉬우니까 다음에도 써먹을 수 있게 메소드 조금 더 소개!
Mockito의 주요 목적은 테스트하는 단위를 고립시키면서 메소드가 예상대로 수행되는지 검증하기 위함이다. 따라서 verify와 then으로 호출 회수나 인자를 검증할 수 있지만, 때로는 메소드에 정확한 인자가 넘어가는지 검사해야 될 때도 있다. 그리고 이를 위해선 자바의 다양한 메소드의 동작 구조를 알고 사용해야 적절한 시기에 적절하게 이용할 수 있다고 생각한다.