코딩/sparta TIL

TIL 52 : 테스트코드에 왜 AuthUser가 포함되지 않는가

americanoallday 2025. 5. 9. 20:50

✅ 먼저:  @Auth AuthUser는 어떻게 들어오는가?

✔️ 실 서비스에서는:

@PostMapping("/todos")
public ResponseEntity<?> saveTodo(@Auth AuthUser authUser, ...)

 

이럴 때 Spring은 

  1. 클라이언트가 HTTP 요청을 보냄 (ex. Authorization 헤더 포함)
  2. Spring은 @Auth라는 커스텀 어노테이션을 보고
  3. HandlerMethodArgumentResolver가 실행돼서
  4. 요청에서 유저 정보를 꺼내고 → AuthUser 객체를 만들어 자동 주입

 

❌ 그런데 테스트(MockMvc)에서는?

mockMvc.perform(post("/todos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(...))

이건 그냥 JSON 본문만 보내는 “단순한 가짜 HTTP 요청”일 뿐

Spring Security 필터도 안 돌고,
Authorization 헤더도 없고,
ArgumentResolver도 등록 안 되어 있음

 

즉, 실서버에서 자동으로 주입되는 AuthUser

MockMvc에서는 아무런 정보도 없이 null이 됨

 

 

🔥 비유

진짜 서비스: “문 앞에 출입증을 보여주면, 경비원이 너의 이름과 권한을 확인해서 컨트롤러에 알려줌”
MockMvc 테스트: “출입증 없이 문을 똑똑 두드리는데, 경비원이 없어” → AuthUser는 당연히 주입되지 않음 = null

 

그냥 직접 작성한 리졸버를 주입시켜서 해결함 ㅎㅎ 밑에 해결방법으로 안됨

@ExtendWith(MockitoExtension.class)
class TodoControllerTest {

    private MockMvc mockMvc;

    @Mock
    private TodoService todoService;

    @InjectMocks
    private TodoController todoController;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(todoController)
            .setCustomArgumentResolvers(new TestAuthUserArgumentResolver())
            .build();
    }

    @Test
    @DisplayName("todo 저장 성공 테스트")
    void todo_저장_성공() throws Exception {
        // given
        AuthUser authUser = new AuthUser(1L, "test@example.com",UserRole.USER);
        User user = User.fromAuthUser(authUser);

        TodoSaveRequest request = new TodoSaveRequest("title","content");
        TodoSaveResponse response = new TodoSaveResponse(1L,"title","content","Sunny",new UserResponse(user.getId(),user.getEmail()));

        // when
        when(todoService.saveTodo(any(AuthUser.class),any(TodoSaveRequest.class))).thenReturn(response);

        // then
        mockMvc.perform(post("/todos")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.title").value("title"));;
    }


    public class TestAuthUserArgumentResolver implements HandlerMethodArgumentResolver {
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(Auth.class);  // 너희 @Auth 조건
        }

        @Override
        public Object resolveArgument(MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) {
            return new AuthUser(1L, "test@example.com", UserRole.USER); // Dummy 유저
        }
    }
}

 


✅ 그래서 해결법은?

해결법 설명
@Auth 제거 테스트용으로 컨트롤러 파라미터 단순화
② ArgumentResolver 목킹 AuthUser를 자동으로 주입하도록 테스트 코드에서 구현
③ 통합 테스트로 전환 실제 인증 로직 포함한 테스트로 변경 (@SpringBootTest + 인증 헤더 등)

 

 

✅ 한 줄 요약

MockMvc는 진짜 필터 체인과 인증/리졸버를 통과하지 않기 때문에, @Auth AuthUser는 직접 주입하거나 흉내내야만 동작 함.

 

테스트코드에 리졸버 클레스 추가해서 해결함

@TestConfiguration             // 🔸 테스트 전용 스프링 설정 클래스
static class ResolverConfig {

    @Bean                     // 🔸 스프링이 자동으로 이 객체를 빈으로 등록
    public HandlerMethodArgumentResolver testAuthArgumentResolver() {
        return new HandlerMethodArgumentResolver() {
            @Override
            public boolean supportsParameter(MethodParameter parameter) {
                return parameter.getParameterType().equals(AuthUser.class);
            }

            @Override
            public Object resolveArgument(MethodParameter parameter,
                                          ModelAndViewContainer mavContainer,
                                          NativeWebRequest webRequest,
                                          WebDataBinderFactory binderFactory) {
                return new AuthUser(1L, "test@email.com", UserRole.USER);
            }
        };
    }
}

 

 

1. @TestConfiguration

  • 테스트에서만 사용하는 설정 클래스
  • 실제 애플리케이션이 실행될 땐 이 설정은 무시됨
  • 내부에 정의된 @Bean 들은 테스트에서만 작동

 

2. @Bean HandlerMethodArgumentResolver

@Bean
public HandlerMethodArgumentResolver testAuthArgumentResolver() {
  • HandlerMethodArgumentResolver는 Spring MVC가 컨트롤러 파라미터를 해석할 때 사용하는 인터페이스
  • 여기서는 @Auth AuthUser를 처리할 리졸버를 직접 정의하는 것

 

3. supportsParameter(...)

public boolean supportsParameter(MethodParameter parameter) {
    return parameter.getParameterType().equals(AuthUser.class);
}
  • 이 리졸버가 어떤 파라미터를 지원할지 판단 즉, AuthUser 타입인 파라미터에만 동작하게 제한함

 

4. resolveArgument(...)

public Object resolveArgument(...) {
    return new AuthUser(1L, "test@email.com", UserRole.USER);
}
  • 실제로 AuthUser 파라미터가 필요할 때 여기를 타고 더미 유저 객체를 주입해줌 (테스트 전용 유저)

 

When에서 에러 발생

when(todoService.saveTodo(any(AuthUser.class), eq(request))).thenReturn(response);

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

 

 

✅ 핵심 메시지

“when using matchers, all arguments have to be provided by matchers”

 

🔥 의미:

  • when(...).thenReturn(...) 에서 메서드의 파라미터를 지정할 때,
  • 어떤 파라미터에 any() 같은 matcher를 썼다면 다른 파라미터도 matcher로 써야 한다!

 

 잘못된 코드 (혼용)

when(todoService.saveTodo(any(), new TodoSaveRequest(...)))
        .thenReturn(response);
  • 첫 번째 인자는 matcher any()
  • 두 번째 인자는 그냥 실제 값
  • 혼용하면 Mockito가 예외 터뜨림

 

 올바른 코드

when(todoService.saveTodo(any(), eq(new TodoSaveRequest("title", "content"))))
        .thenReturn(response);
  • eq(...) = “equal to 이 값이어야 함”이라는 matcher 이걸 써야 any()와 함께 사용할 수 있음

 

리졸버 클래스를 테스트 코드에 만들었는데 인식을 못함.

  • @WebMvcTest는 필요한 컴포넌트만 최소한으로 로딩함
  • 내부 static class인 ResolverConfig는 Spring이 자동으로 스캔하지 않음
  • 그래서 @Import(...)로 명시적으로 등록해야 함
@WebMvcTest(TodoController.class)
@Import(TodoControllerTest.ResolverConfig.class)  // 👈 이거 추가
class TodoControllerTest {
    ...
}