테스트 코드를 작성하는 것의 중요도는 여러 자료들에서 찾아볼 수 있다. 나 역시 개인적으로 테스트 코드를 왜 작성해야하는지, 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 계층의 클래스에서 처리된다. 그러면 도대체 무엇을 테스트 할 수 있을까? 당연히 아무것도 없다. 하는 일이 없는데 뭔가 억지로 테스트를 시키려니 테스트 작성이 어려웠던 것이다.
여기까지 생각이 미치고 나니, 그럼 이런 계층이 필요한건가? 라는 생각에도 도달할 수 있었다. 이는 추후 테스트 코드를 작성하고 리팩토링을 진행하면서 좀 더 설계를 뒤집어보면서 보완해가야 할 것 같다. 물론, 추후 작성할 테스트코드는 구현을 염두에 두지 않고 작성하고자 노력해야할 것이다.
테스트 코드에 대해 공부하면서, 내 생각보다 테스트 코드가 중요한 역할을 하는 것을 알 수 있었다. 어렵게 테스트 코드를 작성해 본 경험이 단순히 '테스트 코드 짜는거 너무 어려워!' 에서 그치지 않고, 테스트 코드를 왜 작성하는지, 어떤 코드가 테스트하기 좋은 코드인지, 테스트 코드를 통해 어디까지 코드스멜을 맡아볼 수 있는지 등을 배울 수 있는 소중한 경험이 되어 만족스럽다.
'Swimming > Think' 카테고리의 다른 글
Spring Security 적용 이후 API 계층 테스트 방법에 대한 생각 (1) | 2023.12.05 |
---|---|
나는 왜 알고리즘 문제를 어려워하는가? (0) | 2023.05.31 |
나는 왜 Mapper를 사용하지 않는가? (0) | 2023.05.31 |
죽이되든 밥이되든 (0) | 2023.04.20 |
스트림은 왜 사용해야 하는가. (1) | 2023.03.08 |
댓글