본문 바로가기
Swimming/Think

나는 왜 테스트 코드 작성을 어려워 했는가?

by JKROH 2023. 8. 23.
반응형

 테스트 코드를 작성하는 것의 중요도는 여러 자료들에서 찾아볼 수 있다. 나 역시 개인적으로 테스트 코드를 왜 작성해야하는지, TDD를 공부해야 하는 이유는 무엇인지에 대해 생각했던 경험이 있다. 최근에는 테스트 코드 작성에 어려움을 겪어 이동욱님의 블로그 글들을 참고하기도 하고, 인프랩이나 카카오의 기술 블로그 글 들을 읽어보았다. 그리고 여러 글들을 참고하고 영상들을 본 끝에, 내가 왜 테스트 코드를 작성하는데 어려움을 겪고 있었는지를 어렴풋이나마 이해할 수 있게 되었고, 이를 위해 기록하고자 한다.

 

 나의 이해를 가장 크게 도운 자료는 권용근님의 스프링캠프 - 무엇을 테스트 할 것인가? 어떻게 테스트 할 것인가? 발표 영상이었다. 테스트 코드를 작성하면서 내가 겪었던 가장 큰 어려움은 '이렇게 짤거면 테스트 코드를 왜 짜는거야?' 라는 생각에서 기인했다. 예를 들어, 프로젝트를 진행하는 동안 작성했던 테스트 코드 중 하나를 살펴보면 다음과 같다.

 

@ExtendWith(MockitoExtension.class)
class CocktailServiceTest {

    @Mock
    private CocktailCreateService cocktailCreateService;

    @Mock
    private CocktailSerializer cocktailSerializer;

    @Mock
    private CocktailDeserializer cocktailDeserializer;

    @Mock
    private UserService userService;

    @InjectMocks
    private CocktailService cocktailService;

    @Test
    void createCocktailTest() {
        // given
        String email = "test@example.com";
        CocktailDto.Post dto = createPostDto();

        User user = mock(User.class);
        Cocktail cocktail = mock(Cocktail.class);
        Tags tags = mock(Tags.class);
        Cocktail savedCocktail = mock(Cocktail.class);

        when(userService.findUserByEmail(email)).thenReturn(user);
        when(cocktailDeserializer.postDtoToEntity(dto)).thenReturn(cocktail);
        when(cocktailCreateService.create(user, cocktail)).thenReturn(savedCocktail);
        when(savedCocktail.getTags()).thenReturn(tags);
        when(savedCocktail.getCocktailId()).thenReturn(1L);
        when(user.getRate(savedCocktail.getCocktailId())).thenReturn(0);
        when(user.getUserId()).thenReturn(1L);

        CocktailDto.Response response = CocktailDto.Response.builder()
                .userId(user.getUserId())
                .cocktailId(savedCocktail.getCocktailId())
                .isBookmarked(false)
                .build();

        when(cocktailSerializer.entityToSignedUserResponse(user, savedCocktail, false, user.getRate(savedCocktail.getCocktailId())))
                .thenReturn(response);

        // when
        CocktailDto.Response result = cocktailService.createCocktail(email, dto);

        // then
        assertThat(user.getUserId()).isEqualTo(result.getUserId());
        assertThat(savedCocktail.getCocktailId()).isEqualTo(result.getCocktailId());
        assertThat(result.isBookmarked()).isEqualTo(false);
        assertThat(result.getRating()).isEqualTo(user.getRate(savedCocktail.getCocktailId()));

        verify(userService).findUserByEmail(email);
        verify(cocktailCreateService).create(user, cocktail);
    }

    private CocktailDto.Post createPostDto() {
        RecipeDto.Post process1 = new RecipeDto.Post();
        process1.setProcess("1");
        RecipeDto.Post process2 = new RecipeDto.Post();
        process2.setProcess("2");
        TagDto.Post tag = new TagDto.Post();
        tag.setTag("sweet");
        IngredientDto.Post ingredient = new IngredientDto.Post();
        ingredient.setIngredient("milk");

        CocktailDto.Post dto = new CocktailDto.Post();
        dto.setName("test");
        dto.setImageUrl("sample image url");
        dto.setLiquor("rum");
        dto.setIngredients(List.of(ingredient));
        dto.setRecipe(List.of(process1, process2));
        dto.setDegree("frequency_high");
        dto.setFlavor(List.of(tag));
        return dto;
    }
}

 

 테스트 코드를 작성하고 가장 처음 든 생각은 '이게 테스트가 맞나?' 였다. 내가 값을 집어넣고, 그 값이 나오게 하는게 무슨 테스트인가. 이런 생각이 들고 나니, 내가 테스트 코드를 작성할 줄 모르는건지, 그게 아니면 뭘 테스트해야하는지를 모르는건지, 그것도 아니면 코드 자체가 잘못된 건지에 대한 파악이 우선되어야겠다고 판단했다.

 

 앞서 언급한 여러 자료들을 참고한 결과, 테스트를 작성하기 어려웠던 가장 큰 이유는 무엇을 테스트해야 하는지를 모르기 때문이라고 결론을 내렸다. 권용근님의 말씀처럼, '설계를 테스트'할 수 있었던 것이다. 위의 테스트는 아래의 메서드를 테스트하기 위한 테스트 메서드이다.

 

    public CocktailDto.Response createCocktail(String email, CocktailDto.Post dto) {
        User user = userService.findUserByEmail(email);
        Cocktail cocktail = cocktailDeserializer.postDtoToEntity(dto);
        Cocktail savedCocktail = cocktailCreateService.create(user, cocktail);
        log.info("# userId : {}, cocktailId : {}, cocktailName : {} CocktailService#createCocktail 성공", user.getUserId(), savedCocktail.getCocktailId(), savedCocktail.getName());
        savedCocktail.assignRecommends(cocktailReadService.readDetailPageRecommendCocktails(savedCocktail.getTags(), savedCocktail.getCocktailId()));
        return cocktailSerializer.entityToSignedUserResponse(user, savedCocktail, BOOKMARK_DEFAULT, user.getRate(savedCocktail.getCocktailId()));
    }

 

 해당 메서드는 DTO와 사용자의 email을 바탕으로 사용자 정보도 찾고, 엔티티를 만들고 저장하기도 하고, 또 새롭게 추천 칵테일들을 읽어오기도 하는 로직들을 포함하고 있다. 나는 이 메서드를 CocktailService라는 클래스에 넣어놓고, 모든 Service 클래스들을 컨트롤 하는 새로운 Layer 로써 사용하고자 하였다. Service계층 자체를 두 개의 계층으로 분리해 Controller 계층에서 직접 콜을 받는 Service 클래스와, 그 Service 클래스가 새롭게 호출하는 세부 Service클래스 들이 있는 형태로 구분하고자 했다.(이들을 앞으로 서비스 1계층, 서비스 2계층 이라고 칭한다.)

 

 이렇게 설계한 결과, 서비스 1계층의 테스트는 대단히 어려워졌다. 외부로부터 넘어오는 제어할 수 없는 값들(DTO, email)이 존재하고, Logger를 통해 외부에 영향을 주기도 한다. 그렇다보니, 당연히 테스트 코드는 막무가내식으로 작성되었을 수밖에 없다는 것이 내 결론이었다. 그런데, 과연 이런 계층에 테스트가 필요할까? 라는 생각이 문득 들었다.

 

 해당 메서드 내에서 자체적으로 실행되는 로직은 아예 없다. 모든 로직이 다른 Service 계층의 클래스에서 처리된다. 그러면 도대체 무엇을 테스트 할 수 있을까? 당연히 아무것도 없다. 하는 일이 없는데 뭔가 억지로 테스트를 시키려니 테스트 작성이 어려웠던 것이다.

 

 여기까지 생각이 미치고 나니, 그럼 이런 계층이 필요한건가? 라는 생각에도 도달할 수 있었다. 이는 추후 테스트 코드를 작성하고 리팩토링을 진행하면서 좀 더 설계를 뒤집어보면서 보완해가야 할 것 같다. 물론, 추후 작성할 테스트코드는 구현을 염두에 두지 않고 작성하고자 노력해야할 것이다.

 

 테스트 코드에 대해 공부하면서, 내 생각보다 테스트 코드가 중요한 역할을 하는 것을 알 수 있었다. 어렵게 테스트 코드를 작성해 본 경험이 단순히 '테스트 코드 짜는거 너무 어려워!' 에서 그치지 않고,  테스트 코드를 왜 작성하는지, 어떤 코드가 테스트하기 좋은 코드인지, 테스트 코드를 통해 어디까지 코드스멜을 맡아볼 수 있는지 등을 배울 수 있는 소중한 경험이 되어 만족스럽다.

반응형

댓글