[Mockito] @MockBean과 @WithMockUser로 인증된 요청 테스트 하기

aqualung·2025년 3월 3일
post-thumbnail

문제 상황

MockMvc으로 애플리케이션의 요청과 응답을 테스트하는 중 문제가 발생했습니다.
JWT토큰이 없는 요청을 제한하기 위해 JwtFilter를 SecurityFilterChain에 등록해 두었습니다.
JwtFilter를 비활성화하여 테스트하는 방법을 작성해 보겠습니다.

예시 코드

@SpringBootTest
@AutoConfigureMockMvc
public class AuthTest {

    @Autowired
    MockMvc mockMvc;

    @TestConfiguration
    static class AuthTestConfiguration {
        @Bean
        public TestController testController() {
            return new TestController();
        }
    }

    @RestController
    static class TestController {

        @GetMapping("/auth-test")
        public String hello(
                @AuthenticationPrincipal UserDetails userDetails
                ) {
            return "hello " + userDetails.getUsername();
        }
    }

    @Test
    void testJwt() throws Exception {

        mockMvc.perform(MockMvcRequestBuilders.get("/auth-test"))
                .andExpect(MockMvcResultMatchers.content().string("hello seungjoon"));
    }
}

/auth-test URI에 접근하면 "hello {username}"를 응답하도록 테스트 코드를 작성했습니다.

TestController에서 @AuthenticationPrincipal UserDetails userDetails 부분은 인증 성공 후 SecurityContext에 저장되어 있는 UserDetails를 ArgumentResolver가 인자로 가져오도록 스프링시큐리티에서 제공하는 어노테이션입니다.

테스트를 실행하면 다음과 같은 결과가 나옵니다.

등록해둔 JwtFilter에서 검증로직 실행 후 JWT토큰이 없다면 발생하는 예외입니다.

JwtFilter 비활성화

간단하게 JwtFilter를 비활성화할 수 있습니다.
@MockitoBean으로 스프링 컨테이너의 JwtFilter를 모킹하는 것입니다.

@SpringBootTest
@AutoConfigureMockMvc
public class AuthTest {

    @Autowired
    MockMvc mockMvc;
    
    /* JwtFilter 모킹 */
    @MockitoBean
    JwtFilter jwtFilter;
    
    생략...

}

이로써 JwtFilter의 모든 토큰 검증 로직은 비활성화됩니다.
테스트를 실행해 봤습니다.

예상과 다르게 응답 status는 200으로 나왔지만 바디가 비어있습니다.

모킹한 JwtFilter는 비어있고 다음 필터를 호출하는 filterChain.doFilter()를 실행하지 않기 때문입니다.

JwtFilter 스터빙

@MockitoBean으로 등록한 JwtFilter가 검증 로직을 실행하지 않고 다음 필터만 실행하도록 스터빙해주겠습니다.

@SpringBootTest
@AutoConfigureMockMvc
public class AuthTest {

   생략...

    @Test
    void testJwt() throws Exception {
    
    	/* stubbing */
    	willAnswer(invocation -> {
            FilterChain filterChain = invocation.getArgument(2);
            filterChain.doFilter(invocation.getArgument(0), invocation.getArgument(1));
            return null;
        }).given(jwtFilter).doFilter(any(), any(), any());

        mockMvc.perform(MockMvcRequestBuilders.get("/auth-test"))
                .andExpect(MockMvcResultMatchers.content().string("hello seungjoon"));
    }
}

이제 JwtFilter는 오직 filterChain.doFilter()로 다음 필터를 실행하는 기능만을 하게 됩니다.

JwtFilter는 비활성화 했지만 아직 인증되지 않은 요청이기 때문에 접근이 제한됩니다.

저는 인증되지 않은 요청이 들어왔을때 해당 응답을 내리도록 시큐리티에 AuthenticationEntryPoint를 등록해 두었습니다.

인증객체 Mocking

요청을 인증된 상태로 만들기 위해서는 SecurityContext에 인증객체 Authentication을 저장해주면 됩니다.
하지만 이러한 기능을 하도록 시큐리티에서 @WithMockUser라는 어노테이션을 제공합니다.

@WithMockUser는 SecurityContext에 UsernamePasswordAuthenticationToken을 인증 객체로 등록해줍니다.
UserDetails를 가지고 있고 어노테이션의 속성을 이용해 유저정보를 설정할 수 있습니다.
value나 username 속성으로 username을 설정할 수 있습니다.

@SpringBootTest
@AutoConfigureMockMvc
public class AuthTest {

   생략...
   
   @RestController
    static class TestController {

        @GetMapping("/auth-test")
        public String hello(
                @AuthenticationPrincipal UserDetails userDetails
                ) {
            return "hello " + userDetails.getUsername();
        }
    }
    
    생략...

    @Test
    @WithMockUser("seungjoon") // 인증객체 Mocking
    void testJwt() throws Exception {
    
    	willAnswer(invocation -> {
            FilterChain filterChain = invocation.getArgument(2);
            filterChain.doFilter(invocation.getArgument(0), invocation.getArgument(1));
            return null;
        }).given(jwtFilter).doFilter(any(), any(), any());

        mockMvc.perform(MockMvcRequestBuilders.get("/auth-test"))
                .andExpect(MockMvcResultMatchers.content().string("hello seungjoon"));
    }
}

이제 인증된 요청이 /auth-test에 접근하게 되고 인증객체에 담긴 UserDetails을 가져와 정상 응답할 수 있습니다.

@WithMockUser를 커스텀하여 UserDetails를 상속 받은 다른 객체를 유저정보로 사용할 수도 있습니다.

0개의 댓글