2021. 12. 23. 13:55ㆍ작업일지
개요
저번 일지에서는 테스트의 종류를 살펴보았다. 이번 일지에서는 기존의 테스트 코드를 살펴보고, 그 테스트 코드의 문제점을 파악하고, 개선하는 작업을 기록해보겠다.
분석
우선 현재 작성된 테스트 코드부터 살펴보자.
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ItemRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ItemService itemService;
@Autowired
private CategoryService categoryService;
@Test
@Order(1)
@DisplayName("1번 상품 조회 성공")
void findItem() throws Exception {
mockMvc.perform(
get("/api/item/{id}", 1)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").exists());
}
@Test
@Order(2)
@DisplayName("단건 상품 조회 실패")
void failFindItem() throws Exception {
mockMvc.perform(
get("/api/item/{id}", 99)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.id").doesNotExist())
.andExpect(jsonPath("$.statusCode").value(HttpStatus.NOT_FOUND.getReasonPhrase()))
.andExpect(jsonPath("$.message").exists());
}
...기타 등등...
}
코드가 너무 길어서 중간에 잘랐는데, 테스트를 통해 검증되는 내역은 다음과 같다.
@SpringBootTest
: 실제 애플리케이션이 작동될 때, 사용하는 모든Bean
을 로드하여, 애플리케이션 컨텍스트가 제대로 작동하는지 확인한다.MockMvc
: Spring MVC(@Controller
가 선언된 클래스의 핸들러 메소드들)가 의도한대로 작동하는지 확인한다.ObjectMapper, ItemService, CategoryService
:1.
을 통해 형성된 빈을 가져와서 의도한 로직대로 작동하는 지 확인한다.즉, 위 테스트를 통해, 빈의 의존관계를 체크하고, 지정된 uri에 따라 올바른 응답이 리턴되는지 확인할 수 있다.
MockMvc
의 api를 처음 사용해보는 입장에서, 다소 헷갈린 부분이 많았지만, 익숙해지니 생각보다, 직관적으로 활용할 수 있도록 api가 구성되어있다는 것을 알 수 있었다. 게다라가,@Service
빈과ObjectMapper
를 사용하는 것은 기존의 서비스 로직을 사용하는 방식과 크게 다를 게 없어서, 코드를 작성하는 데는 큰 문제가 없었다.위의 테스트 코드를 저번 일지에서 익혔던 테스트의 종류를 기준으로 분류해보자면,
통합 테스트
라고 할 수 있겠다. 순서에 따라, 테스트가 진행되었다는 점에서인수 테스트
로 분류할지 고민도 해보았지만, 뚜렷한, 시나리오가 있는 것이 아니고, 비즈니스 측면에서 바라본 테스트가 아니라, 기능의 검증이 목적인 테스트라고 판단하여,통합 테스트
로 분류하기로 했다. 이러한 테스트의 경우, 한번에, 애플리케이션 컨텍스트와 서비스 로직, 웹 계층의 응답 메세지의 유효성까지 보장하는 테스트이기에, 테스트 커버리지가 높은 테스트라고 할 수도 있겠다.하지만, 위의 테스트의 경우는.. 저번 일지에서도 언급했듯이, CI를 통해 빌드하는 시간을 너무 잡아먹는다. 컨텍스트를 로드하는 데 걸리는 시간이 가장 길었는데,
Item
영역만 테스트를 했을 경우 다음과 같은 시간이 소요된다.
최초 CI 빌드 시간이 40-50초 사이였던 점을 감안하면, 위와 같은 패턴의 테스트가 증가한다면, 분명, 테스트를 하는데 소요되는 시간은 기하급수적으로 증가할 것이고.. 이 부분은 애초에 테스트 코드를 작성하여 얻는 장점 중 하나인 '빠른 피드백을 받을 수 있다'는 부분을 잃게된다.
해결 - 통합 테스트 -> 슬라이스 테스트
위의 테스트는 크게 컨텍스트
, 서비스
, 웹
이 세가지를 모두 검증한다. 그런데, 꼭 하나의 테스트에 세가지를 모두 검증해야하는 지는 조금 의문이다. 컨텍스트의 경우, CI 과정을 통해 빌드하는 과정에서 체크할 수 있다.
빌드하는 과정에서, 컨텍스트 설정이 잘못되었을 경우, 위의 이미지 처럼, 실패 메세지를 보낸다.
웹 계층의 경우는, 아직 배포가 안되었다는 점을 감안한다면, api 디자인이 확정된 것도 아닌데, 벌써부터 테스트 코드로 검증할 필요는 없다고 본다. 작동여부를 확인하고 싶다면, 테스트 환경에서 직접 작동시켜보는 방법도 있다.
즉, 현 시점에서는 서비스 계층을 로직만 검증하는 테스트 코드를 작성하면 된다. 이렇게 계층별로, 레이어를 나누고 단일 계층만 테스트를 하는 것을 슬라이스 테스트
라고 한다.
Mockito로 테스트를 전환하기
서비스 계층의 로직을 테스트하는 툴 중에서 가장 보편적인 것으로 Mockito를 소개해볼 수 있을 것 같다. Mockito는 @Mock
과 같은 관련 애노테이션을 활용하여, 실제로는 작동하지 않지만, 로직이 논리적으로, 혹은 의도한 기능대로 흘러가는 지 확인해볼 수 있는 테스트 라이브러리이다. @SpringBootTest
를 통해 컨텍스트를 로드하는 과정을 거치지 않아도 서비스 로직의 유효성을 검증할 수 있다는 점에서, 테스트 시간을 많이 줄일 수 있을거라 생각했다. 게다가, 인터페이스를 mocking 하여, JUnit 테스트에 적용할 수도 있는데, 개발 초기에 협업을 할 경우, 인터페이스 설계를 먼저 하는 경우가 많다는 점을 감안하면, 꽤 요긴하게 쓰일 수 있다.
Mockito를 적용하여 수정한 코드는 다음과 같다.
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
@Mock
ItemRepository itemRepository;
@InjectMocks
ItemService itemService;
@Test
@DisplayName("상품 생성")
void createItem() {
given(itemRepository.save(NEW_ITEM)).willReturn(true);
itemService.saveItem(NEW_ITEM);
then(itemRepository).should(times(1)).save(NEW_ITEM);
}
@Test
@DisplayName("단건 상품 조회")
void findItem() {
given(itemRepository.findById(anyLong())).willReturn(ITEM);
itemService.findItemById(ID).get();
then(itemRepository).should(times(1)).findById(ID);
}
@Test
@DisplayName("상품 수정")
void updateItem() {
given(itemRepository.save(ITEM)).willReturn(true);
itemService.saveItem(ITEM);
then(itemRepository).should(times(1)).save(ITEM);
}
@Test
@DisplayName("상품 목록 조회")
void findItems() {
SimplePageRequest simplePageRequest = new SimplePageRequest();
given(itemRepository.findAll(simplePageRequest)).willReturn(anyList());
itemService.findItems(simplePageRequest);
then(itemRepository).should(times(1)).findAll(simplePageRequest);
}
}
테스트 코드를 작성하는 스타일은 다양하지만, 개인적으로는 BDD (given, when, then) 방식을 선호한다. 인과관계를 명시적으로 표시할 수 있다는 점을 장점으로 채택하였기 때문이다. 이렇게 테스트 코드를 작성하고, 다시 CI를 작동시켜본 결과 다음과 같은 결과를 가질 수 있었다.
77초에서 54초로 줄어든 것을 볼 수 있다. 애플리케이션 컨텍스트를 로드하는 과정을 생략할 수 있었기 때문이다. 물론, 워크플로우에서, Maven clean
을 생략하고, cache
기능을 적용하면, 빌드 시간을 더 줄일 수 있을 것이다. 하지만, 아직 프로젝트가 개발단계라, 패키지 구조가 변경되는 경우를 수용하기 위해 우선은 clean
을 하여 jar
를 초기화 시켜서 verify
하는 과정을 밟았다.
마무리
이번 문서를 통해, 하나의 테스트에 애플리케이션의 모든 계층을 테스트를 했었던 부분을 발견하고, 중복 체크하는 부분(애플리케이션 컨텍스트 로딩)과 아직은 테스트하지 않아도 되는 부분(웹 계층)을 분리하여, 순수 서비스 계층의 코드만 테스트하는 방향으로 변경 해보았다. 이 과정을 통해, 테스트를 직관적으로 진행할 수 있었고, 모델의 필드를 변경하는 과정에서 발생했던 기존 데이터 액세스 로직이 꼬여서 테스트케이스가 꼬였던 케이스를 피할 수 있었다.
또한 테스트 코드를 경량화하여, 결과적으로 CI 빌드시간을 줄일 수 있었던 것이 가장 큰 이점이었다. 물론, 애플리케이션 코드의 양이 많아지고, 그 코드를 검증할 테스트 코드가 많아지면서, 빌드 시간이 늘어날 수 있겠지만, Mockito의 경우 첫 테스트에만 300 - 400ms가 소요되고, 나머지는 2~5 ms 정도가 소요되는 것을 보아, 그렇게 큰 폭으로 증가할 것 같진 않다.
사실 그동안 테스트 코드를 작성할 때, 인터넷 강의에서 가르쳐준 보편적인 테스트 코드에 의존하여, 분별력없이 테스트 코드를 막 작성하였다. 이러한 행동으로 인해, CI의 비효율적인 성능을 야기하게 되었는데, 항상 느끼는 거지만, 코드의 사용법을 익히는 것에만 급급하고, 익숙한 방향대로만 코드를 치려고 하는 행위는 당장은 편해도 나중에 대가를 치루게 되는 것 같다.
'작업일지' 카테고리의 다른 글
작업 일지#11 배포준비 (0) | 2022.02.10 |
---|---|
작업 일지#10 작업 방향 계획 변경 (0) | 2022.01.26 |
작업 일지#8 테스트 개선 -1- (0) | 2021.12.09 |
작업 일지#7 서비스에 Security를 적용해보자 -3- 나의 로직에 시큐리티를 얹어보자 (0) | 2021.11.26 |
작업 일지#6 서비스에 Security를 적용해보자 -2- 스프링 시큐리티의 구조 이해하기 (0) | 2021.11.25 |