인프런 커뮤니티 질문&답변

비가싫어요님의 프로필 이미지
비가싫어요

작성한 질문수

호돌맨의 요절복통 개발쇼 (SpringBoot, Vue.JS, AWS)

Controller Test 시 @WebMvcTest 사용 관련 질문입니다.

작성

·

967

0

안녕하세요 호돌맨님 :) 좋은 강의 늘 감사드립니다.

Controller Test 할 때 질문이 있습니다. 강의에서 호돌맨님께서는 @SpringBootTest 사용하셨는데 저는 한 번 @WebMvcTest를 사용해보고 있습니다.

그러다보니 문제가 하나 발생을 하더라구요. 우선 PostController 쪽 코드를 보여드리겠습니다.

@GetMapping("/posts")
public ApiResponse<List<PostResponse>> getAll(@ModelAttribute PostSearch postSearch) {
    List<PostResponse> posts = postService.getAllPost(postSearch);
    return ApiResponse.ok(posts);
}

보시면 게시글을 전체 조회하는 이 메서드의 경우에는 인증이 따로 필요없이 바로 조회가 되게 했는데요. Insomnia 라는 프로그램을 통해서 테스트했을 때 따로 인증하지 않아도 전체 조회 요청이 성공하는 것을 확인했습니다.

그런데 테스트코드를 작성할 때 인증 처리를 해주지 않으면 계속해서 401 에러가 발생을 하더라구요.

@DisplayName("게시글을 전체 조회한다.")
@CustomMockUser //todo 실제로는 인증이 필요없는데도 테스트 통과를 위해서 인증 처리를 해줘야 하는 것인가?
@Test
void getAllPosts() throws Exception {
    // given
    List<Post> posts = IntStream
       .range(1, 5)
       .mapToObj(i -> Post.of(FREE, i + "번 제목입니다.", i + "번 내용입니다."))
       .toList();

    List<PostResponse> response = changePostListToPostResponseList(posts);

    // when
    when(postService.getAllPost(any())).thenReturn(response);

    //then
    mockMvc.perform(
          MockMvcRequestBuilders.get("/api/posts")
       )
       .andDo(MockMvcResultHandlers.print())
       .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
       .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("OK"))
       .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("OK"))
       .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray())
       .andExpect(MockMvcResultMatchers.status().isOk());
}

@CustomMockUser 어노테이션을 붙여줘야 테스트 통과가 되는 상황입니다.

실제 원하는 기능은 인증없이 전체 조회 되게 하는 것이 목표인데 테스트 통과를 위해서 @CustomMockUser 어노테이션을 붙여주는 것이 올바른 테스트인지 궁금합니다!

 

혹시 이게 @WebMvcTest와 관련없는 다른 문제라면 알려주시면 한 번 제가 더 찾아보겠습니다..!

답변 2

0

호돌맨님의 프로필 이미지
호돌맨
지식공유자

안녕하세요. 호돌맨입니다.
Insomnia 는 제가 사용해본적이 없습니다. ㅠㅠ REST 테스트 도구인가요?

SecurityConfig쪽 코드가 잘못 되어있을 가능성이있습니다.

프로젝트 소스를 올려주시면 보도록 하겠습니다.

감사합니다.

@Configuration
@EnableWebSecurity(debug = true)
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig {

    private final ObjectMapper objectMapper;
    private final UserRepository userRepository;

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
       return web -> web.ignoring()
          .requestMatchers("/favicon.ico")
          .requestMatchers("/error");
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       return http
          .csrf(AbstractHttpConfigurer::disable)
          .authorizeHttpRequests()
          .anyRequest().permitAll()
          .and()
          .addFilterBefore(customEmailPasswordFilter(), UsernamePasswordAuthenticationFilter.class)
          .exceptionHandling(e -> {
             e.accessDeniedHandler(new Http403Handler(objectMapper));
             e.authenticationEntryPoint(new Http401Handler(objectMapper));
          })
          .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
       return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(UserRepository userRepository) {
       return username -> {
          User user = userRepository.findByEmail(username)
             .orElseThrow(() -> new UsernameNotFoundException(username + "을 찾을 수 없습니다."));

          return new UserPrincipal(user);
       };
    }

    @Bean
    public CustomEmailPasswordFilter customEmailPasswordFilter() {
       CustomEmailPasswordFilter filter = new CustomEmailPasswordFilter(objectMapper);
       filter.setAuthenticationManager(authenticationManager());
       filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
       filter.setAuthenticationFailureHandler(new LoginFailureHandler(objectMapper));
       filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
       return filter;
    }

    @Bean
    public AuthenticationManager authenticationManager() {
       DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
       provider.setUserDetailsService(userDetailsService(userRepository));
       provider.setPasswordEncoder(passwordEncoder());
       return new ProviderManager(provider);
    }
}
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class MethodSecurityConfig {

    private final PostRepository postRepository;

    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
       DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
       handler.setPermissionEvaluator(new BoardPermissionEvaluator(postRepository));
       return handler;
    }
}

SecurityConfig 관련된 클래스들입니다.

 

테스트 코드입니다. 한 개는 @SpringBootTest 사용했고 한 개는 @WebMvcTest 사용했습니다. @WebMvcTest로 하면 @CustomMockUser 어노테이션이 없으면 통과가 되질 않습니다. 401 에러가 나면서요!

@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest
class AuthControllerTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void tearDown() {
       userRepository.deleteAllInBatch();
    }

    @DisplayName("회원가입 한다.")
    @Test
    void signUp() throws Exception {
       //given
       SignUpRequest request = SignUpRequest.builder()
          .name("zun")
          .email("zun@test.com")
          .password("12345")
          .nickname("zunny")
          .build();

       //expected
       mockMvc.perform(post("/api/auth/sign-up")
             .content(objectMapper.writeValueAsString(request))
             .contentType(APPLICATION_JSON)
          )
          .andExpect(status().isOk())
          .andDo(print());
    }

}
@ActiveProfiles("test")
@AutoConfigureMockMvc
@WebMvcTest(AuthController.class)
class AuthControllerTestWithWebMvcTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    private AuthService authService;

    @MockBean
    private UserRepository userRepository;


    @DisplayName("회원가입 한다.")
    @CustomMockUser // 회원가입을 해야되는데 인증이 필요하다? -> 논리적으로 말이 안 됨 -> 올바른 테스트?
    @Test
    void signUp() throws Exception {
       // given
       SignUpRequest request = SignUpRequest.builder()
          .name("zun")
          .email("zun@test.com")
          .password("12345")
          .nickname("zunny")
          .build();

       // then
       mockMvc.perform(post("/api/auth/sign-up")
             .content(objectMapper.writeValueAsString(request))
             .contentType(APPLICATION_JSON)
             .with(csrf())
          )
          .andExpect(status().isOk())
          .andDo(print());
    }

}
호돌맨님의 프로필 이미지
호돌맨
지식공유자

님 빨리 github 링크도 주세요

호돌맨님의 프로필 이미지
호돌맨
지식공유자

아.. @WebMvcTest로 진행하게 되면 님이 만드신 스프링시큐리티 쪽 설정을 다 안들고 실행됩니다.
그래서 csrf나 스프링시큐리티 필터등이 기본 값으로 들어가 있을겁니다.

해결 1.

mockMvc.perform(post("/auth/signup")
                .with(SecurityMockMvcRequestPostProcessors.csrf()) // <-- 요 부분 추가
                .content(objectMapper.writeValueAsString(signup))
                .contentType(APPLICATION_JSON))

해결 2.

@AutoConfigureMockMvc(addFilters = false) // addFilters = false 추가

 

이렇게 함 해보세용

감사합니다.

헐헐 너무 감사합니다 :D

0

안녕하세요, 인프런 AI 인턴이에요. @WebMvcTest를 사용해서 controller를 테스트할 때 인증 처리를 해주지 않으면 401 에러가 발생하는 것은 정상입니다. @WebMvcTest는 해당 컨트롤러의 테스트를 진행하기 위해 필요한 Spring MVC 구성 요소만 로드하기 때문에, 보안과 관련된 설정들은 로드하지 않습니다.

인증 처리를 하지 않고도 테스트를 통과하기 위해서는 직접 인증이 필요 없는 사용자를 생성하거나, @WithMockUser 애노테이션을 사용하여 테스트용 사용자를 만들어서 테스트 메소드에 적용시킬 수 있습니다.

예를 들어, @WithMockUser 애노테이션을 사용하여 테스트용 사용자를 생성하는 방법은 아래와 같습니다.

@DisplayName("게시글을 전체 조회한다.")
@WithMockUser(username = "testuser", roles = "USER")
@Test
void getAllPosts() throws Exception {
    // 테스트 코드 작성
}

위의 예시에서는 testuser라는 사용자를 생성하고, 해당 사용자에게는 USER 역할이 할당되어 있는 상태로 테스트 코드를 진행합니다. 이렇게 하면 인증 처리를 따로 해줄 필요 없이 테스트를 진행할 수 있습니다.

추가로, @WithMockUser 애노테이션에는 username, roles, password 등 다양한 속성을 설정할 수 있습니다. 필요에 따라 적절히 설정하시면 됩니다.

참고로, 위의 예시는 Spring Security의 @WithMockUser 애노테이션을 사용한 예시입니다. 따라서 Spring Security를 사용하고 있지 않거나 다른 인증 방식을 사용하고 있다면, 해당 프레임워크에서 제공하는 인증 처리 방법을 사용해야 합니다.

비가싫어요님의 프로필 이미지
비가싫어요

작성한 질문수

질문하기