실수에서 배우는 테스트를 통한 책임 분리

비구름·2025년 4월 24일

Spring

목록 보기
8/10

상황 설명

평소처럼 테스트 케이스를 작성하여 토이 프로젝트를 진행하던 도중 갑자기 에러가 날 찾아왔다.

jakarta.servlet.ServletException: Request processing failed: org.mockito.exceptions.misusing.PotentialStubbingProblem: 

문제가 생긴 코드는 다음과 같다.

final PutDailyRequestDTO body = new PutDailyRequestDTO("title", "content", LocalDate.of(2020, 12, 12));
doThrow(new BusinessException(DailyErrorCode.DAILY_NOT_FOUND))
          .when(dailyService).update(DailyModifyDTO.of(100, body));

위 코드는 크게 문제가 없어보인다. 설명을 해보자면 id 100을 가진 Daily를 body에 맞게 수정하는 api의 컨트롤러를 테스트하는 코드이다.

따라서 RequestBody에 필요한 객체를 만들고 컨트롤러, 서비스 간의 데이터 교환을 위한 dto로 변환해주는 of메서드를 사용하여 쉽게 변환했다.

크게 문제가 없어보이는 코드 뭐가 문제일까?

문제 분석

우선 로그를 천천히 읽어보자

두 번째 줄의 설명을 읽어보면 Strict stubbing argument mismatch.라고 친절하게 적혀있다. Stub의 인자가 일치하지 않다는 것이다. 하지만 내부적으로 동작하는 메서드를 그대로 넣었기 때문에 큰 문제가 없을 것이라고 생각했으나 자바는 기본적으로 객체의 동일함을 equals메서드를 통해 판단한다. 기본적으로 제공하는 equals는 객체의 주소값이 같아야하므로 당연히 다른 객체라고 인식할 수 밖에 없다.

문제 해결

크게 문제해결 방법은 2가지다.

문제 해결 - equals 생성

문제가 생긴 DTO에 Data 어노테이션을 넣는다면 쉽게 해결이 가능하다.

@Getter
@Builder
@Data
public class DailyModifyDTO {
	...
}

위 사진에서 볼 수 있듯이 테스트는 성공했다. 그럼 이 부분은 해결이 된걸까?

문제 제기

아직 부족하다. DTO는 데이터를 넘기는 것에 대한 책임을 가지고 있지만 DTO 내부 데이터의 비교를 통한 DTO의 동일함까지 판단하는 책임을 가지고 있지 않다. 따라서 equals메서드가 들어가기 어렵다고 판단했다. 기존 프로젝트에 필요하지 않은 코드이기도 할 뿐더러 DTO는 데이터를 전달해준다는 책임만을 충실히 이행하면 된다.

그럼 어떻게 해결할 수 있을까?

문제 해결 - any() 메서드 사용

any 메서드를 사용하면 바로 해결이 가능하다.

doThrow(new BusinessException(DailyErrorCode.DAILY_NOT_FOUND))
            .when(dailyService).update(any(DailyModifyDTO.class));

문제가 생긴 DTO는 해당 클래스에 정확한 타입의 객체가 오는지 확인만 하면 된다. 현재 컨트롤러는 해당 변환 과정에 대한 책임이 존재하지 않는다. 만약 존재하더라도 코드는 DTO에게 동일성의 확인 책임을 전가하는 형태로 컨트롤러에서 테스트를 진행하게 될 것이다. 따라서 컨트롤러의 테스트에서는 해당 테스트가 진행되지 않아도 괜찮다고 판단했다. 다행히도 Mockito에서는 any 메서드를 허용한다. 따라서 우리는 해당 객체라면 어떤 것이든 상관없다라는 코드로 any 메서드를 사용하여 처리한다.

이렇게 문제 해결인가?

아니다. 우리는 아직 내부적으로 데이터가 제대로 변환되는지 확인을 하지 못했다. 지금은 간단하게 Builder를 통해 변환하는 동작이라 사실 테스트까지는 필요없지만 공부용 프로젝트이기에 이러한 변환도 한번 해결할 필요성은 있다. 위에서 해당 변환 과정은 dto의 책임이라고 말을 했다. 그럼 해당 변환 과정에 대한 테스트를 진행하기 위해서 우리는 DTO에 대한 테스트를 추가한다.

@Test
@DisplayName("Put 요청 제대로 변환되는지 확인")
void ofPutDailyRequestDTO() {
    // given
    int id = 10;
    String title = "title";
    String content = "content";
    LocalDate deadline = LocalDate.of(2020, 2, 2);
    PutDailyRequestDTO requestDTO = new PutDailyRequestDTO(title, content, deadline);

    // when
    DailyModifyDTO dto = DailyModifyDTO.of(id, requestDTO);

    // then
    assertEquals(id, dto.getId());
    assertEquals(title, dto.getTitle());
    assertEquals(content, dto.getContent());
    assertEquals(deadline, dto.getDeadline());
}

크게 어려운 코드는 아니다. 단순히 입력 데이터를 주고 그에 맞는 데이터가 정확하게 변환이 되었는지 확인하는 코드이다. 내부는 Builder로 이루어져 예상가능한 코드라고 생각한다.

결론

이렇게 우리는 Stub의 인자 문제를 객체지향의 책임을 생각하게 되며 여기까지 리팩토링을 하게 됐다.

우리는 이 과정을 통해 단순히 테스트를 작성한 것이 아닌 테스트를 작성함으로서 각 객체의 책임을 생각하고 올바르게 책임이 분리된 테스트와 객체를 얻을 수 있었다.

번외

만약 DTO에 Data를 넣지않고 any도 사용하지 않는다면 일어나는 코드를 GPT가 짜준 것을 첨부합니다.

doThrow(new BusinessException(DailyErrorCode.DAILY_NOT_FOUND))
    .when(dailyService)
    .update(argThat(dto -> dto.getId() == 100 && dto.getTitle().equals("title")));

한눈에 봐도 코드의 가독성은 떨어지고 title뿐이 아닌 content, deadline까지 비교를 하게 된다면 더 길어질 것입니다. 또한 DTO의 변화로 getId나 내부 데이터가 바뀐다면 컨트롤러의 테스트를 수정해야하는 유지보수를 위해 테스트를 작성하였지만 유지보수가 힘든 테스트 코드를 작성할 것입니다.

코드

DTO
DTO 테스트
컨트롤러
컨트롤러 테스트

느낀점

처음에는 TDD를 공부하기 전 단순히 테스트 코드에 익숙해져보자고 시작한 프로젝트였습니다. 하지만 기존에 생각하지 않았던 부분을 생각하고 제가 작성한 코드를 다시 보며 객체지향적인 코드인가 그리고 유지보수하기 좋은 코드인가라는 고민을 끊임없이 하게 되었습니다. 아직 테스트에 대한 지식은 없지만 D2에서 본 글을 통해 프로젝트를 진행하며 얻은 경험의 질을 높여가고 있습니다. 단순히 테스트 코드를 작성하며 만난 예외를 처리하는 과정에서 객체지향의 책임 분리까지 오게되며 많은 생각이 들었던 경험이였습니다.

profile
기본부터 정리해나가는 기록용 블로그

0개의 댓글