제가 요즘 Spring Security를 이용해서 간단한 인증서버 구축을 하고 있습니다
인증서버 테스트 코드 작성을 하면서 겪은 문제점에 대해서 나눠보고자 합니다
1. CustomSecurityFilter를 작성했을 경우 테스트 구성
먼저는 테스트를 구성하는 것부터 문제가 발생했습니다
일단 저는 JWT 인증을 구현하기 위해서 자체적으로 Custom 한 JwtAuthTokenFilter라는 것을 구성했습니다
그래서 일반적으로 Spring MVC 테스트를 위해서 사용하는 @WebMvcTest를 사용했을 경우에 문제가 발생했습니다
@WebMvcTest에 대해서 간단하게 설명하자면 Spring MVC의 컴포넌트들의 초점을 맞춰서 테스트를 진행하는 어노테이션으로 보면 됩니다
(MVC 컴포넌트는 @Controller, @JsonComponent, @Filter, @WebMvcConfigurer 등이 있습니다)
일단 초기 테스트를 진행할때의 저의 코드입니다 (여기서 Spring Security Filter 추가할 때 참고한 곳은 아래에 링크로 남겨두었습니다)
@WebMvcTest(value = AuthenticationController.class, includeFilters = @ComponentScan.Filter(classes = {EnableWebSecurity.class}))
public class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private JwtAuthTokenFilter jwtAuthTokenFilter;
@MockBean
private UserDetailsServiceImpl userDetailsService;
@MockBean
private JwtAuthEntryPoint jwtAuthEntryPoint;
private final SignUpDTO signUpDTO = new SignUpDTO("baek0318@icloud.com", "1234", "baek", "USER");
@Test
@DisplayName("회원가입을 진행시에 올바른 응답값이 나오는지 확인")
@WithMockUser
void signUpTest() throws Exception {
String content = objectMapper.writeValueAsString(signUpDTO);
mockMvc.perform(post("/api/auth/signup")
.content(content)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
}
}
이렇게 작성하면 굉장한 에러들을 만나게 됩니다
일단 이 테스트 코드에서의 문제점은 @WebMvcTest만 가지고 특정 컨트롤러를 테스트한다고 했을 때에 그 컨트롤러가 다른 객체에 대한 의존성을 가지고 있다면은 해당 의존성은 @WebMvcTest가 불러오지 않는다는 것입니다 그래서 위와같은 에러들을 만나게 됩니다
2. 의존성 해결책
1. 모든 의존성을 불러온다 -> 거의 불가능...
의존성의 문제가 있다면 해당 의존성들을 다 불러와주면 되지 않을까 라고 생각하게 되지만 한 의존성을 부르면 의존성 -> 의존성 -> 의존성... 이런식으로 꼬리에 꼬리를 물기 때문에 몇 개 없다면 상관없지만 많다면 거의 불가능까지는 아니지만 힘들 가능성이 큽니다
2. @MockBean을 이용해서 여기서 의존하고 있는 Service계층을 모의로 만들어 준다 -> 가능성이 존재
@WebMvcTest(value = AuthenticationController.class, includeFilters = @ComponentScan.Filter(classes = {EnableWebSecurity.class}))
public class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AuthenticationService authenticationService;
@MockBean
private JwtAuthTokenFilter jwtAuthTokenFilter;
@MockBean
private UserDetailsServiceImpl userDetailsService;
@MockBean
private JwtAuthEntryPoint jwtAuthEntryPoint;
private final SignUpDTO signUpDTO = new SignUpDTO("baek0318@icloud.com", "1234", "baek", "USER");
@Test
@DisplayName("회원가입을 진행시에 올바른 응답값이 나오는지 확인")
@WithMockUser
void signUpTest() throws Exception {
String content = objectMapper.writeValueAsString(signUpDTO);
mockMvc.perform(post("/api/auth/signup")
.content(content)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
}
}
좋은 방법이여서 도입을 했습니다
그래도 어느정도 희망이 보이는 것은 모의 요청을 만들어서 보내는 것 까지는 성공했습니다
하지만 에러가 발생하게 됩니다 위의 에러메세지에 대해 대략 내용을 얘기하자면 아까 위에서 JwtAuthTokenFilter라는 제가 정의해준 SecurityFilter가 모의 객체라서 발생하는 에러라고 보면 됩니다
그러면 Mockito에 있는 given() 이나 when()을 이용해서 처리를 해주면 되지 않을까 라고 생각했지만
Filter에는 반환값이 있는 메서드가 없고 proteted 접근제어자가 있는 메서드만 존재함으로 어떻게 처리해줘야 할지 애매하게 되었습니다...
3. JwtAuthTokenFilter 해결책
JwtAuthTokenFilter만 진짜 객체를 주입해주자
이것을 달성하기 위해서는 2가지 정도의 방법이 있다고 생각이 됩니다
1. Integration Test 로 작성하기
//@WebMvcTest(value = AuthenticationController.class, includeFilters = @ComponentScan.Filter(classes = {EnableWebSecurity.class}))
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AuthenticationService authenticationService;
@Autowired
private JwtAuthTokenFilter jwtAuthTokenFilter;
@MockBean
private UserDetailsServiceImpl userDetailsService;
@MockBean
private JwtAuthEntryPoint jwtAuthEntryPoint;
private final SignUpDTO signUpDTO = new SignUpDTO("peachberry@kakao.com", "1234", "baek", "USER");
@Test
@DisplayName("회원가입을 진행시에 올바른 응답값이 나오는지 확인")
void signUpTest() throws Exception {
String content = objectMapper.writeValueAsString(signUpDTO);
given(authenticationService.signup(signUpDTO))
.willReturn(SignUpSuccessDTO.builder()
.email("peachberry@kakao.com",)
.name("baek")
.id(1L)
.role(Role.USER).build());
mockMvc.perform(post("/api/auth/signup")
.content(content)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
}
}
일단 기존의 @WebMvcTest는 주석처리를한 다음에 @SpringBootTest, @AutoCofigureMockMvc, @Transactional 어노테이션을 작성해 줍니다
이 상태로 테스트를 진행하게 되면
위와 같이 테스트를 통과하고 제대로 응답까지 나오는 것을 확인할 수 있습니다
하지만 이 테스트의 문제점을 보자면은 DB까지 연결을 시켜줘야지만 테스트가 돌아간다는 것입니다...
그리고 Spring의 문서를 봐도 @SpringBootTest, @AutoCofigureMockMvc이 2개의 조합으로 돌아가는 것은 서버를 실제로 작동시키지만 않을 뿐이지 실제 환경과 거의 흡사한 테스트를 진행한다고 합니다
그러므로 이 테스트는 좀 더 Integration Test에 가깝다고 보는 것이 맞는것 같습니다
2. 좀 덜 Integration Test 작성
Unit Test는 아니지만 데이터 베이스를 실제로 작동 시키지 않고 테스트하는 방법에 대해서 작성해보려고 합니다
여기서는 @WebMvcTest와 @Import 어노테이션을 사용해서 만들어 보겠습니다
@WebMvcTest(value = AuthenticationController.class, includeFilters = @ComponentScan.Filter(classes = {EnableWebSecurity.class}))
@Import({JwtAuthTokenFilter.class})
public class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AuthenticationService authenticationService;
@MockBean
private JwtUtil jwtUtil;
@MockBean
private CookieUtilImpl cookieUtil;
@MockBean
private UserDetailsServiceImpl userDetailsService;
@MockBean
private JwtAuthEntryPoint jwtAuthEntryPoint;
private final SignUpDTO signUpDTO = new SignUpDTO("peachberry@kakao.com", "1234", "peach", "USER");
@Test
@DisplayName("회원가입을 진행시에 올바른 응답값이 나오는지 확인")
@WithMockUser
void signUpTest() throws Exception {
String content = objectMapper.writeValueAsString(signUpDTO);
given(authenticationService.signup(signUpDTO))
.willReturn(SignUpSuccessDTO.builder()
.email("peachberry@kakao.com")
.name("peach")
.id(1L)
.role(Role.USER).build());
mockMvc.perform(post("/api/auth/signup")
.content(content)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
verify(authenticationService, times(1)).signup(signUpDTO);
}
}
@Import를 사용한 이유는 JwtAuthTokenFilter는 모의가 아니라 진짜 객체로 만들어주기 위해서 입니다
나머지 의존성들은 아까와 마찬가지로 MockBean으로 설정해주면 됩니다
이렇게 설정을 해주게 되면 DB를 작동시키지 않아도 테스트를 진행할 수 있습니다
짧지만 간단하게 어떤 방식으로 Spring Security의 CustomFilter가 있을 경우에 테스트하는 방법에 대해서 알아보았습니다
더 좋은 Unit Test 방법을 알고 계신 분이 계시다면 알려주시면 감사하겠습니다!!
참고
WebMvcTest 참고 : docs.spring.io/spring-framework/docs/current/reference/html/testing.html#spring-mvc-test-framework
SpringBootTest 참고 : meetup.toast.com/posts/124
Spring Security 설정 참고 : blog.devenjoy.com/?p=524
Spring Mvc Test 참고 : spring.io/guides/gs/testing-web/