ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] MockMvc 테스트에서 with(csrf()) 자동화 하기
    Backend/테스트 코드 2024. 10. 11. 09:29

     

    1. 사건 발달

    스프링 시큐리티 환경에서 MVC를 테스트하기 위해서는 요청 헤더에 CSRF 토큰이 필요하다. 따라서 아래 코드와 같이 perform() 시에 요청 헤더에 스프링 시큐리티가 생성한 CSRF 토큰을 포함할 수 있다.

    근데 테스트 코드를 작성할 때마다 CSRF 토큰을 일일이 추가하는 것보단, 테스트 실행 전에 자동으로 추가할 수 있지 않을까? 

    @WithMockUser
    @WebMvcTest(controllers = SectionController.class)
    class SectionControllerTest {
        @Test
        @DisplayName("신규 회고카드를 등록한다.")
        void createSection() throws Exception {
            //given
            CreateSectionRequest request = CreateSectionRequest.builder()
                .retrospectiveId(1L)
                .templateSectionId(2L)
                .sectionContent("내용")
                .build();
    
            //when //then
            mockMvc.perform(
                    post("/sections")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(APPLICATION_JSON)
                        .with(csrf())
                )
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value("201"))
                .andExpect(jsonPath("$.message").doesNotExist());
        }
    }

     

     

    2. 요청 헤더에 CSRF 토큰 자동으로 추가하기

    @BeforeEachJUnit5에서 제공하는 애너테이션으로, 각 테스트가 실행되기 전에 호출된다. 이 애너테이션을 사용하여 모든 테스트가 실행되기 전에 CSRF 토큰을 추가하도록 해보자.

    • (1) MockMvcBuildersMockMvc 객체를 생성할 때 사용되는 유틸리티 클래스이다.
    • (2) 실제 웹 애플리케이션 컨텍스트(WebApplicationContext)를 사용하여 MockMvc 객체를 생성한다.
      • WebApplicationContext는 테스트할 때, Spring의 실제 웹 애플리케이션 환경을 모방하기 위해 사용된다.
    • (3) Spring Security 설정을 MockMvc에 적용한다.
      • springSecurity()SecurityMockMvcConfigurers의 메서드로, Spring Security의 필터 체인이 MockMvc 요청에 대해 적용되도록 설정한다.
      • 실제 웹 애플리케이션에서처럼 인증, 인가, CSRF 보호 등의 보안 기능을 테스트할 수 있게 된다.
    • (4) POST 요청의 모든 엔드포인트에 대하여 CSRF 토큰을 자동으로 포함한다.
      • defaultRequest()는 모든 테스트에서 사용될 요청 정보를 설정한다. 
      • post(), get() 이런 식으로 HTTP 메서드를 지정할 수 있다.
        • ex) post("/**") : 모든 경로("/**")에 대해 POST 요청을 대상으로 설정을 수행한다.
    @WithMockUser
    @WebMvcTest(controllers = SectionController.class)
    class SectionControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Autowired
        private WebApplicationContext applicationContext;
    
        @BeforeEach
        public void setUp() {
            mockMvc = MockMvcBuilders  // (1)
                .webAppContextSetup(applicationContext)  // (2)
                .apply(springSecurity())  // (3)
                .defaultRequest(post("/**").with(csrf()))  // (4)
                .build();
        }
        
    }

     

    3. 결론

    @BeforeEach에서 MockMvc 설정을 통해 매번 테스트 코드에 with(csrf())를 수동으로 추가하는 번거로움을 줄일 수 있다. 

    @WithMockUser
    @WebMvcTest(controllers = SectionController.class)
    class SectionControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Autowired
        private WebApplicationContext applicationContext;
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @BeforeEach
        public void setUp() {
            mockMvc = MockMvcBuilders
                .webAppContextSetup(applicationContext)
                .apply(springSecurity())
                .defaultRequest(post("/**").with(csrf()))
                .build();
        }
    
        @Test
        @DisplayName("신규 회고카드를 등록한다.")
        void createSection() throws Exception {
            //given
            CreateSectionRequest request = CreateSectionRequest.builder()
                .retrospectiveId(1L)
                .templateSectionId(2L)
                .sectionContent("내용")
                .build();
    
            //when //then
            mockMvc.perform(
                    post("/sections")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value("201"))
                .andExpect(jsonPath("$.message").doesNotExist());
        }
    }

     

Designed by Tistory.