JUnit5과 Spring boot 테스트 코드 작성-Controller Layer 단위 테스트와 인증 인가 테스트 (4)

taehee kim·2023년 4월 4일
0

0. 서론

  • 시리즈의 이전 포스트에서 Service Layer를 테스트 하는 방법에 대해 고민하면서 test double 사용 트레이드 오프와 컨트롤 할 수 없는 코드 영역을 상위 모듈로 유도해야 테스트에 유리하다는 것에 대해서 이야기 해보았습니다.
  • 이번 글에서는 Controller 에서는 어떤 부분을 테스트해야할 지 어떻게 테스트 환경을 구축해야할 지에 대해서 이야기 해보려고 합니다.

1. Controller layer에서 무엇을 테스트해야할까?

  • 이를 말하기에 앞서 Spring Web Mvc의 구조와 Servelet Filter에 대해서 파악해 보는 과정이 필요할 것 같습니다.

1-1. Servlet Filter

  • Servlet Filter는 서블릿 컨테이너에 존재하는 개념으로 Servlet을 호출하기에 앞서서 미리 요청을 거르는 역할을 합니다.
  • 이와 관련된 공통 관심사를 처리할 수 있으며 Spring Security를 사용할 경우 이와 관련된 검증은 Spring Security FilterChain 에서 수행됩니다.
  • Spring에서는 서블릿으로 Dispatcher Servlet이라는 것이 가장 앞단에 위치합니다.

1-2. Spring Web Mvc

  • 사용자 요청이 오면 Filter를 거치고 필터를 통과하면 Spring의 DispatcherServlet을 호출합니다.
  • Dispatcher Servlet에서 요청에 적합한 Handler adapter와 Handler를 선택하여 호출하게 되고 이때 Handler가 우리가 작성한 Controller에 해당합니다.

1-3. Controller 테스트 시에 테스트 범위

  • 위와 같은 구조를 설명한 이유는 Controller테스트 시에는 Controller내부의 코드 뿐만 아니라 filter부터 Handler 내부에 이르기까지 테스트 범위가 수행될 수 있기 때문입니다.
  • 따라서 테스트 목적에 따라 이를 구분하고 적절히 Bean들을 생성하여 테스트 범위를 나누어 테스트하는 것으로 정하였습니다.

1-4. Controller 테스트 내용

Controller에서 테스트 되는 내용은 이렇게 볼 수 있습니다.

  • 입력값 Validation
  • Controller Advisior에 의해 Exception이 적절히 상태코드로 변경되는지 여부
  • Filter에서의 로직
  • 전체 통합 테스트

Validation

2. 테스트 구현 방법

2-1. @WebMvcTest

Bean 생성 범위

@Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).

  • Controller, ControllerAdvice
  • Filter
  • 요청에 따라 입력값을 변환하고 handle를 찾아줄 @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver등이 Bean으로 생성된다.

controllers field

  • @WebMvcTest(controllers = {.class}) 와 같이 특정 Controller만 로딩하여 테스트할 수 있다.

@AutoConfigureMockMvc

  • MockMvc를 자동 설정 해준다.

2-2. @MockMvc

  • 실제 사용 요청이 오는 것처럼 테스트해볼 수 있게 해주는 Annotation이다.

2-3. @WithMockUser

  • SecurityContext에 UsernamePasswordAuthenticationToken를 추가해준다. 즉, 가상으로 인증된 유저 요청처럼 꾸밀 수 있다.

2-3. 사용 사례

입력값 Validation만 검증하고 싶은 경우

@WebMvcTest(value = {RandomMatchController.class},
    excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
    }
)
@AutoConfigureMockMvc(addFilters = false)
class RandomMatchControllerTest {

    @Autowired
    private MockMvc mockMvc;
}
  • SecurityConfig는 Security Filter Chaing을 설정해주는 class이다 이를 제외한다.
  • MockMvc를 생성할 때 Filter를 등록하지 않는다. Spring Security Filter chain에 속하지 않는 필터를 포함하지 않기 위해서 필요하다.

Filter를 검증하고 싶은 경우

  • WebMvcTestWithSecurityDefaultConfig를 통해 공통적으로 등록되어야 하는 Bean들을 등록해준다.
  • MockMvc를 주입받지 않고 직접 생성하여 사용한다. WebApplicationContext를 주입받아 설정하고 MockMvcConfigurer.springSecurity()로 스프링 Security Filter Chain을 등록한다.
@TestConfiguration
@Import({CustomOAuth2UserService.class, DefaultOAuth2UserServiceConfig.class,
    CustomAuthenticationEntryPoint.class,
    RedirectAuthenticationSuccessHandler.class, RedirectAuthenticationFailureHandler.class})
public class WebMvcTestWithSecurityDefaultConfig {

}
@WebMvcTest(ArticleController.class)
@Import(WebMvcTestWithSecurityDefaultConfig.class)
public class ArticleControllerWithSecurityTest {
    private MockMvc mockMvc;
    @Autowired
    private WebApplicationContext context;

    @BeforeEach
    private void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
            .apply(springSecurity())
            .build();
    }

인증된 유저인것처럼 요청하고 싶을 때

    @Test
    @WithMockUser(username = "username", authorities = {"article.create"})
    void writeArticle_whenHasAuthority_then200() throws Exception {

MockMvc 기본 기능

  • perform에서 요청을 정의하고 andExpect에서 응답을 정의하여 테스트할 수 있다.
 @Test
    void writeArticle_whenNotAuthenticated_then401() throws Exception {
        ArticleDto articleDto = ArticleDto.builder()
            .title("title")
            .date(LocalDate.now().plusDays(1))
            .matchConditionDto(MatchConditionDto.builder()
                .placeList(List.of())
                .timeOfEatingList(List.of())
                .wayOfEatingList(List.of())
                .build())
            .content("content")
            .anonymity(false)
            .participantNumMax(1)
            .contentCategory(ContentCategory.MEAL)
            .build();
        mockMvc.perform(post("/api/articles")
                .contentType(MediaType.APPLICATION_JSON).content(
                    new ObjectMapper().registerModule(new JavaTimeModule())
                        .writeValueAsString(articleDto)))
            .andDo(print())
            .andExpect(status().isUnauthorized());
    }
profile
Fail Fast

0개의 댓글