통합테스트에서 Unit Test로 변경하여 구현하기 위해 Controller Unit 테스트코드를 작업하는 중 org.springframework.beans.factory.UnsatisfiedDependencyException
가 발생하였다. @WebMvcTest
를 사용한 이유는 @SpringBootTest
를 사용하면 실제 어플리케이션 설정을 모두 로드하기 때문에 어플리케이션 규모가 커지면 느려지기 때문이다. Controller 단위테스트에만 집중하기 위해 @WebMvcTest
를 사용했다. 무튼 UnsatisfiedDependencyException
를 해결하기 위한 방법, 더 나아가 WebMvcTest 와 Spring Security를 함께 사용했을 때 발생했던 모든 문제들에 대한 트러블슈팅을 포스팅 한다.
@WebMvcTest
는 컨트롤러와 관련된 Bean을 자동으로 configuration 한다.
자동으로 Configuration 하는 빈들은 다음과 같다.
@Controller
@ControllerAdvice
@JsonComponent
Converter
GenericConverter
Filter
WebMvcConfigurer
WebSecurityConfigurerAdapter
HandlerMethodArgumentResolver
WebMvcConfigurer 에서 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 문서를 먼저 읽고 솔루션을 찾는게 빨리 문제를 해결할 수 있을 거 같다.
좋은 정보 잘 보고 갑니다.
감사합니다.