Album 프로젝트의 테스트들은 모두 스프링 로드 하에 실행된다.
Presentation 계층의 테스트나 Rest Docs 테스트는 @WebMvcTest를 통해 등록되는 빈을 줄이고 Mock을 사용하긴 하지만, 스프링을 구동하기는 마찬가지이다.
이러한 스프링 구동 하에 실행되는 테스트의 장점은 실제 서비스 환경과 매우 유사한 환경에서 테스트를 실행해볼 수 있다는 것이다.
하지만 단점도 있는데, 스프링을 로드하는 과정 때문에 테스트 속도가 비교적 느리다는 것이다.
이러한 단점을 보완할 수 있는 방법이 있는 지 여러 블로그들의 글을 뒤적이다가 알게된 사실이 있다.
어찌보면 당연하지만 스프링 통합 테스트의 속도 문제는 스프링 로드 횟수에 가장 큰 영향을 받으며, 각 테스트들이 공통된 환경에서 실행되도록 구현한다면 불필요한 스프링 로드를 줄일 수 있다는 것이다.
이것을 가능하게 해주는 기능이 Context Caching이다.
스프링은 테스트 실행 시 테스트가 사용하는 context를 캐싱한다.
그리고 동일한 context를 사용하는 테스트는 새롭게 스프링을 로드하여 context를 구성하는 것이 아니라, 이전에 캐싱된 context를 사용한다.
여러 가지 요인들에 의해 context cache key가 구성되고,
동일한 cache key를 가진 테스트들이 해당 cache key에 해당 하는 context를 함께 사용하게 된다.
가장 대표적인 요인들은 아래와 같다.
- ActiveProfiles: 어떤 Profiles를 사용하는 지
- MockBean, SpyBean: 어떤 Bean을 Mocking 하는지
- Classes: 어떤 클래스들을 로드하는지
- Property: 어떤 Property를 사용하는 지
실제 적용
현재 Album 프로젝트의 테스트 코드는 크게 Persistence, Business, Presentation, Docs 테스트로 구분되어 있다.
전체 테스트를 실행하는데 총 5.367초가 소요되었으며, 스프링은 총 14번 로드되었다.
실행 환경이 다른 테스트 클래스들이 많다보니, 전체 테스트를 수행할 때 스프링 로드 횟수가 생각보다 많았다.
전체 테스트를 수행할 때 스프링 로드 횟수를 줄이고, 그것을 통해 소요 시간을 감소시킬 필요성이 느껴졌다.
Persistence, Business, Presiontation, Docs 테스트 클래스 별로 테스트 실행 환경을 통합하여, 전체 테스트를 수행할 때 스프링이 총 4회만 로드되도록 개선 방향을 잡았다.
Persistence 테스트 환경 통합
Persistence 테스트 클래스들이 공통적으로 사용할 테스트 환경을 RepositoryTestSupport에 정의하고,
Persistence 테스트 클래스들이 RepositoryTestSupport를 상속하도록 하였다.
@Import(RepositoryTestConfig.class)
@ActiveProfiles("test")
@DataJpaTest
public abstract class RepositoryTestSupport {
@Autowired
protected TestFileManager testFileManager;
@AfterEach
void cleanUp() {
testFileManager.deleteTestFile();
}
}
Business 테스트 환경 통합
Business 테스트 클래스들이 공통적으로 사용할 테스트 환경을 ServiceTestSupport에 정의하고,
Business 테스트 클래스들이 ServiceTestSupport를 상속하도록 하였다.
@Import(ServiceTestConfig.class)
@ActiveProfiles("test")
@Transactional
@SpringBootTest
public abstract class ServiceTestSupport {
@MockBean
protected AwsS3Manager awsS3Manager;
@Autowired
protected TestFileManager testFileManager;
@AfterEach
void cleanUp() {
testFileManager.deleteTestFile();
}
}
Presentation 테스트 환경 통합
Presentation 테스트 클래스들이 공통적으로 사용할 테스트 환경을 ControllerTestSupport에 정의하고,
Presentation 테스트 클래스들이 ControllerTestSupport를 상속하도록 하였다.
@Import(ControllerAndDocsTestConfig.class)
@ActiveProfiles("test")
@WebMvcTest(controllers = {AdminController.class, MemberController.class, FollowController.class,
FeedController.class, CommentController.class},
includeFilters = @ComponentScan.Filter(classes = {EnableWebSecurity.class}))
public abstract class ControllerTestSupport {
@Autowired
protected WebApplicationContext context;
@Autowired
protected ObjectMapper objectMapper;
protected MockMvc mockMvc;
@Autowired
protected FileDir fileDir;
@Autowired
protected DefaultImage defaultImage;
@Autowired
protected TestPrincipalDetailsService testPrincipalDetailsService;
protected PrincipalDetails memberPrincipalDetails;
protected PrincipalDetails adminPrincipalDetails;
@MockBean
protected MemberService memberService;
@MockBean
protected FollowService followService;
@MockBean
protected FeedService feedService;
@MockBean
protected CommentService commentService;
@MockBean
protected AlbumUtil albumUtil;
@MockBean
protected FormLoginSuccessHandler formLoginSuccessHandler;
@MockBean
protected FormLoginFailureHandler formLoginFailureHandler;
@MockBean
protected OAuthLoginSuccessHandler oAuthLoginSuccessHandler;
@MockBean
protected OAuthLoginFailureHandler oAuthLoginFailureHandler;
@Autowired
protected TestFileManager testFileManager;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
memberPrincipalDetails =
(PrincipalDetails) testPrincipalDetailsService.loadUserByUsername("member");
adminPrincipalDetails =
(PrincipalDetails) testPrincipalDetailsService.loadUserByUsername("admin");
}
@AfterEach
void cleanUp() {
testFileManager.deleteTestFile();
}
}
Docs 테스트 환경 통합
마찬가지로 Docs 테스트 클래스들이 공통적으로 사용할 테스트 환경을 DocsTestSupport에 정의하고,
Docs 테스트 클래스들이 DocsTestSupport를 상속하도록 하였다.
@ActiveProfiles("test")
@Import(ControllerAndDocsTestConfig.class)
@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(controllers = {AdminController.class, MemberController.class, FollowController.class,
FeedController.class, CommentController.class})
public abstract class DocsTestSupport {
@Autowired
protected WebApplicationContext context;
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected FileDir fileDir;
@Autowired
protected DefaultImage defaultImage;
@Autowired
protected TestPrincipalDetailsService testPrincipalDetailsService;
protected PrincipalDetails memberPrincipalDetails;
protected PrincipalDetails adminPrincipalDetails;
protected BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@MockBean
protected MemberService memberService;
@MockBean
protected FollowService followService;
@MockBean
protected FeedService feedService;
@MockBean
protected CommentService commentService;
@MockBean
protected AlbumUtil albumUtil;
@MockBean
protected FormLoginSuccessHandler formLoginSuccessHandler;
@MockBean
protected FormLoginFailureHandler formLoginFailureHandler;
@MockBean
protected OAuthLoginSuccessHandler oAuthLoginSuccessHandler;
@MockBean
protected OAuthLoginFailureHandler oAuthLoginFailureHandler;
@Autowired
protected TestFileManager testFileManager;
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(documentationConfiguration(provider))
.build();
memberPrincipalDetails =
(PrincipalDetails) testPrincipalDetailsService.loadUserByUsername("member");
adminPrincipalDetails =
(PrincipalDetails) testPrincipalDetailsService.loadUserByUsername("admin");
}
@AfterEach
void cleanUp() {
testFileManager.deleteTestFile();
}
}
결과
테스트 클래스들의 테스트 환경을 통합한 결과, 전체 테스트에서 스프링은 총 4번 로드되었다.
예상 외로 전체 테스트 실행 시간은 5.340초로 전후 차이가 거의 없었다.
아마 실행 환경이 통합된 테스트 클래스들의 수가 효과가 나타날 만큼 많지 않았기 때문인 것으로 추측된다.
한 가지 생각해볼 것은 테스트 환경을 통합하는 과정으로 인해 각 테스트 클래스들에게 불필요한 bean들 까지 등록된다는 것이다.
특정한 테스트만 실행할 때에는 이것이 불필요하게 큰 context를 구성하여, 테스트 속도가 느려질 수 있다는 단점이 될 수 있다.
그러나 전체 테스트를 실행할 때에는 테스트 환경의 통합으로 인해 불필요한 스프링 로드를 줄일 수 있다는 점에서 장점이 될 수 있다.
context caching은 이러한 장단점을 고려하여, 개발 특성에 맞게 적용해야할 것 같다.