WebMvcTest와 Spring Security 함께 사용하기

Sera Lee·2022년 3월 16일
5

트러블슈팅

목록 보기
1/2
post-custom-banner

개요

통합테스트에서 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 가 빈생성이 되지 않았다는 에러다.

WebSecurityConfigWebSecurityConfigurerAdapter 를 상속받고 있다.

해결방법1

  • AuthenticationEntryPoint.class가 인식이 되도록 includeFilters 로 설정해준다.
@WebMvcTest(controllers = MemberController.class,
    includeFilters = {        
	    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuthenticationEntryPoint.class)
})

해결방법2

  • WebSecurityConfigWebSecurityConfigurerAdapter 를 스캔에서 제외되도록 한다.
    • 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 이다.

발생원인

  • CsrfFilterdoFilterInternal 에 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 를 보면 SecurityContextImplNull 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 문서를 먼저 읽고 솔루션을 찾는게 빨리 문제를 해결할 수 있을 거 같다.

post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 1월 11일

좋은 정보 잘 보고 갑니다.
감사합니다.

답글 달기