통합테스트에서 Unit Test로 변경하여 구현하기 위해 Controller Unit 테스트코드를 작업하는 중 org.springframework.beans.factory.UnsatisfiedDependencyException 가 발생하였다. @WebMvcTest 를 사용한 이유는 @SpringBootTest 를 사용하면 실제 어플리케이션 설정을 모두 로드하기 때문에 어플리케이션 규모가 커지면 느려지기 때문이다. Controller 단위테스트에만 집중하기 위해 @WebMvcTest를 사용했다. 무튼 UnsatisfiedDependencyException 를 해결하기 위한 방법, 더 나아가 WebMvcTest 와 Spring Security를 함께 사용했을 때 발생했던 모든 문제들에 대한 트러블슈팅을 포스팅 한다.
@WebMvcTest 는 컨트롤러와 관련된 Bean을 자동으로 configuration 한다.
자동으로 Configuration 하는 빈들은 다음과 같다.
@Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterWebMvcConfigurerWebSecurityConfigurerAdapterHandlerMethodArgumentResolverWebMvcConfigurer 에서 interceptor 에서 사용할 서비스를 주입받고 있었는데, 이 서비스가 @Service 어노테이션으로 선언되어 있고, @WebMvcTest 는 이를 스캔하지 못해 빈생성이 제대로 되지 않아 발생한 것이였다.
@WebMvcTest(controllers = MemberController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class)})
public class MemberControllerTests
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 를 상속받고 있다.
AuthenticationEntryPoint.class가 인식이 되도록 includeFilters 로 설정해준다.@WebMvcTest(controllers = MemberController.class,
includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuthenticationEntryPoint.class)
})
WebSecurityConfig 나 WebSecurityConfigurerAdapter 를 스캔에서 제외되도록 한다.@WebMvcTest(controllers = MemberController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebSecurityConfigurerAdapter.class)})
public class MemberControllerTests
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());
@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
SecurityContextImpl 이 Null authentication 임이 확인된다.
@WithMockUser어노테이션을 사용하기
인증이 된 상태로 테스트를 진행된다(SecurityContextHolder에 UsernamePasswordAuthenticationToken이 담긴 상태를 만들어 준다.)
SecurityContextPersistenceFilter 에 breakpoint 를 걸어서 SecurityContext를 확인해 보면 authentication 에 UsernamePasswordAuthenticationToken 이 할당되어 있는 것을 확인 할 수 있다.


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 문서를 먼저 읽고 솔루션을 찾는게 빨리 문제를 해결할 수 있을 거 같다.
좋은 정보 잘 보고 갑니다.
감사합니다.