Spring REST Docs 작성을 위한 ControllerTest를 단위테스트로 변경했던 과정을 돌아본다.
큰 동작을 달성하기 위해 여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트
개발자가 변경할 수 없는 부분(외부 라이브러리)까지 묶어서 검증할 때 사용한다.
보통 Spring Boot에서는 SQL을 활용하여 테스트데이터를 넣어두고, API를 호출하여 원하는 응답을 받는지 테스트해본다. Coontroller - Service - Repository 등의 모든 Class에 실제 객체를 사용하여 테스트한다.
응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트
보통 Spring Boot에서는 Controller, Service, Repository 등 각각의 Class들에 한정되어 테스트한다. 해당 Class에 필요한 다른 객체들은 Mock 등을 활용하여 가짜 객체를 사용하여 테스트한다.
@SpringBootTest
를 사용하는 통합테스트로 진행중이였다.
MockMvc
를 활용해 API를 호출하고 API의 전체 동작(Controller, Service, Repository, ...)이 성공하면 REST Docs를 작성했다.
통합테스트로 진행한 방식의 문제점
@SpringBootTest
로 각 테스트마다 Context를 전부 띄워서 진행하니 테스트 시간이 오래걸린다.처음에는 Context 띄우는 시간을 줄이기 위해 @SpringBootTest
를 @WebMvcTest
로 변경하고자 했다.
@WebMvcTest
는 많은 Spring REST Docs 참고자료들이 진행하고 있는 방법이기도 하며, MVC를 위한 테스트로 컨트롤러가 예상대로 동작하는지 테스트하는데 사용된다.
@WebMvcTest
는 다음 Bean들만 스캔하도록 제한한다.
해당 시도는 다른 테스트에 필요한 Spring Config들을 불러오지 못해서 에러가 발생했다.
@SpringBootTest
로 진행하되, 외부 API 및 DB와의 의존도를 제거한다.
당시 통합테스트 진행중 시간이 가장 오래걸린 것중 하나는 DB와의 의존도였다. DB의 형상관리를 위해 liquibase를 사용중이였는데, 통합테스트로 모든 Context를 띄우면 liquibase 로딩이 진행되서 시간이 오래걸린다는 점이였다.
@MockBean
을 활용하여 서비스 객체를 가짜 객체로 선언하여 서비스 로직의 결과값을 원하는대로 제어했다. 가짜 객체로 선언되었기 때문에 서비스 로직을 실행하지 않아서 DB와의 의존도가 줄어된다.
서비스의 Authorization 문제는 Interceptor를 Bean처리 진행하고 나서 Test에서 MockBean으로 Interceptor의 행동을 제어하고 Context를 임의로 지정했다. 이로 인해 외부 API를 요청하던 주된 요인인 액세스토큰의 의존도를 제거했다.
하지만 위의 방법들로 인해 테스트를 진행하는 속도는 빨라졌지만, @SpringBootTest
로 인해 Context를 띄워서 liquibase로 DB와의 Connection을 연결하는 과정에서 문제가 발생한다. 짧은시간내에 DB로 Connection이 너무 많이 요청되어 Too many Connectino 에러가 발생한 것이다.
그래서 결국 돌고 돌아 모든 Context를 띄우는 @SpringBootTest
는 제거하고 MockitoExtension
만 활용하는 단위테스트로 변경했다.
테스트를 진행하는 Controller에 @InjectMocks
로 Mock들을 주입받을 변수임을 지정하고, 필요한 나머지 객체들에 @Mock
을 선언하여 가짜 객체를 주입한다.
mockMvc 설정을 standaloneSetup()
으로 진행하여 해당 Controller에 대한 테스트만 진행하도록 설정한다. ([Spring/번역] 컨트롤러 테스트 가이드 in Spring Boot)
테스트 상에서는 해당 API에서 실행되는 Service 등 객체들의 모든 메소드의 Return값을 설정해야 한다. - doReturn().when().someMethod();
에러 발생 테스트를 진행하기 위해 setControllerAdvice
추가했다.
.setMessageConverters(new MappingJackson2HttpMessageConverter(mapper))
406 Not Acceptable
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
(no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
.registerModule(new ParameterNamesModule())
로 해결..setValidator(new CustomLocalValidatorFactoryBean(Collections.singletonList(new PathKeyValidation(forbiddenPathKeyStorage))))