WebMvcTest와 Spring Security 함께 사용하기
개요
통합테스트에서 Unit Test로 변경하여 구현하기 위해 Controller Unit 테스트코드를 작업하는 중 org.springframework.beans.factory.UnsatisfiedDependencyException
가 발생하였다. @WebMvcTest
를 사용한 이유는 @SpringBootTest
를 사용하면 실제 어플리케이션 설정을 모두 로드하기 때문에 어플리케이션 규모가 커지면 느려지기 때문이다. Controller 단위테스트에만 집중하기 위해 @WebMvcTest
를 사용했다. 무튼 UnsatisfiedDependencyException
를 해결하기 위한 방법, 더 나아가 WebMvcTest 와 Spring Security를 함께 사용했을 때 발생했던 모든 문제들에 대한 트러블슈팅을 포스팅 한다.
발생이유와 해결법
발생 1
발생이유
@WebMvcTest
는 컨트롤러와 관련된 Bean을 자동으로 configuration 한다.
자동으로 Configuration 하는 빈들은 다음과 같다.
@Controller
@ControllerAdvice
@JsonComponent
Converter
GenericConverter
Filter
WebMvcConfigurer
WebSecurityConfigurerAdapter
HandlerMethodArgumentResolver
WebMvcConfigurer 에서 interceptor 에서 사용할 서비스를 주입받고 있었는데, 이 서비스가 @Service
어노테이션으로 선언되어 있고, @WebMvcTest
는 이를 스캔하지 못해 빈생성이 제대로 되지 않아 발생한 것이였다.
해결방법
- WebMvcConfigurer 에서 사용되어 문제가 되는 Service 를 includeFilters 로 ComponentScan 하도록 해봤지만 또 그 Service가 사용하는 다른 Service나 Component 들 때문에 계속 문제가 되었다.
- 💎 그럼 차라리, WebMvcConfigurer 를 스캔되지 않도록 하자. excludeFilters 를 이용하여 이를 충족시킬 수 있다.
@WebMvcTest(controllers = MemberController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class)})
public class MemberControllerTests
발생 2
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webSecurityConfig’
....
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.security.web.AuthenticationEntryPoint' available: expected at least 1 bean which qualifies as autowire candidate
...
No qualifying bean of type 'org.springframework.security.web.AuthenticationEntryPoint' available:
AuthenticationEntryPoint
를 인식하지 못해, WebSecurityConfig
가 빈생성이 되지 않았다는 에러다.
WebSecurityConfig
는 WebSecurityConfigurerAdapter
를 상속받고 있다.
해결방법1
AuthenticationEntryPoint.class
가 인식이 되도록 includeFilters 로 설정해준다.
@WebMvcTest(controllers = MemberController.class,
includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuthenticationEntryPoint.class)
})
해결방법2
WebSecurityConfig
나WebSecurityConfigurerAdapter
를 스캔에서 제외되도록 한다.- WebSecurityConfig 는 WebSecurityConfigurerAdater 를 상속하므로 WebSecurityConfgurerAdater 스캔을 제외하면 당연히 WebSecurityConfig 도 스캔되지 않는다.
- Security 에 대한 테스트코드가 아니므로 Security 관련 코드들은 필요없다. 필요없는 코드들은 제거하여 Controller TestCode 를 얇게 만들자.
@WebMvcTest(controllers = MemberController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebSecurityConfigurerAdapter.class)})
public class MemberControllerTests
발생 3 (StatusCode : 403)
- StatusCode가 403 이다.
발생원인
CsrfFilter
의doFilterInternal
에 breakpoint 를 걸어서 확인하면, csrfToken 이 null 값으로 filterChain.doFilter() 를 수행하지 못하고, AccessDeniedException(MissingCsrfTokenException)을 반환하게 된다.
해결방법
mockMvc.perform()
에.with(SecurityMockMvcRequestPostProcessors.csrf())
with문 추가한다.
mockMvc.perform(post(uri)
.contentType(MediaType.APPLICATION_JSON)
.with(SecurityMockMvcRequestPostProcessors.csrf())
.content(objectMapper.writeValueAsString(createMember)))
.andExpect(status().isOk())
.andDo(print());
- 다음과 같이 csrfToken 이 들어가 있는 것을 확인할 수 있다.
발생 4 (StatusCode : 401)
- 테스트코드를 다음처럼 작성하였는데 401 StatusCode를 준다
@DisplayName("멤버_생성_요청_200_OK")
@Test
public void createMember() throws Exception {
// given
String uri = "/api/members";
String memberId = "createMember";
String name = "createName";
Long projectId = 1L;
MemberDto.CreateMemberRequest createMember = MemberDto.CreateMemberRequest.builder()
.memberId(memberId)
.name(name)
.keyCode(UUID.randomUUID().toString())
.projectId(projectId).build();
// when
mockMvc.perform(post(uri)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createMember)))
.with(SecurityMockMvcRequestPostProcessors.csrf())
.andExpect(status().isOk())
.andDo(print());
}
java.lang.AssertionError: Status expected:<200> but was:<401>
Expected :200
Actual :401
발생이유
- 권한 에러
- SecurityContextPersistenceFilter 를 보면
SecurityContextImpl
이Null authentication
임이 확인된다.
해결방법
@WithMockUser
어노테이션을 사용하기
인증이 된 상태로 테스트를 진행된다(SecurityContextHolder에 UsernamePasswordAuthenticationToken이 담긴 상태를 만들어 준다.)
SecurityContextPersistenceFilter 에 breakpoint 를 걸어서 SecurityContext를 확인해 보면 authentication 에 UsernamePasswordAuthenticationToken 이 할당되어 있는 것을 확인 할 수 있다.
완성된 코드
- gradle에
spring-security-test
의존성 추가 잊지말자
testImplementation group: 'org.springframework.security', name: 'spring-security-test'
@WebMvcTest(controllers = MemberController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebSecurityConfigurerAdapter.class)})
public class MemberControllerTests {
@DisplayName("멤버_생성_요청_200_OK")
@Test
@WithMockUser
public void createMember() throws Exception {
// given
String uri = "/api/members";
String memberId = "createMember";
String name = "createName";
Long projectId = 1L;
MemberDto.CreateMemberRequest createMember = MemberDto.CreateMemberRequest.builder()
.memberId(memberId)
.name(name)
.keyCode(UUID.randomUUID().toString())
.projectId(projectId).build();
// when
mockMvc.perform(post(uri)
.contentType(MediaType.APPLICATION_JSON)
.with(SecurityMockMvcRequestPostProcessors.csrf())
.content(objectMapper.writeValueAsString(createMember)))
.andExpect(status().isOk())
.andDo(print());
}
}
💎 결론
WebMvcTest
로 컨트롤러 단위테스트를 짜는 것이 이렇게도 복잡할 줄이야... 여러 블로그들로부터 해결법에 대해 참조를 해서 해결을 했고, 각 필터들에 break point 를 찍고 확인하면서, 실제 WebMvcTest
위에서 Spring Security
가 동작하는 부분을 이해할 수 있었다. 아무리 봐도 API 명세서를 보는 것은 너무너무 중요한 거 같다. WebMvcTest
클래스의 API 문서를 보면 Scan 되는 Bean 들이 자세히 설명되어 있고, 이외에도 Junit4
를 사용했을 때 주의사항도 명시되어 있었다. 구글링보다는 API 문서를 먼저 읽고 솔루션을 찾는게 빨리 문제를 해결할 수 있을 거 같다.
Author And Source
이 문제에 관하여(WebMvcTest와 Spring Security 함께 사용하기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@cieroyou/WebMvcTest와-Spring-Security-함께-사용하기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)