Spring MVC 컨트롤러 테스트 시 @WebMvcTest를 사용한다. @WebMvcTest는 전체 컨텍스트를 띄우지 않고 웹계층에서 필요한 빈만 로드하여 테스트할 수 있다.
(@Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigure, HandlerMethodArgumentResolver Bean 등이 빈으로 등록되어 의존성 주입됨)
Service와 같은 외부 빈들은 메모리에 없기 때문에 Mock객체로 만들어 테스트하는데 활용한다. @MockitoBean 어노테이션을 사용하여 가짜 객체를 만들어줘야한다. (이전에는 @MockBean으로 사용하였지만 스프링부트 3.4.0 버전 이후부터 deprecated 되어 @MockitoBean으로 대체되었음)
아래와 같이 given(준비)/when(실행)/then(검증) 형식을 맞추어 controller 테스트코드를 작성한다.
@WebMvcTest(controllers = SellerProductController.class)
@Import({JwtTokenProvider.class, SecurityConfig.class})
@DisplayName("SellerProductController MockMvc 테스트")
class SellerProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private UserDetailsService userDetailsService;
@MockitoBean
protected SellerProductService sellerProductService;
private ObjectMapper objectMapper = new ObjectMapper();
@Test
@WithMockCustomUser(id = 1L, roles = {"판매자"})
@DisplayName("T1-(1). 판매자 상품 등록 성공 테스트 - 등록 상품과 200 OK를 반환한다.")
void registerProduct_Return200() throws Exception {
// given 1
ProductResponseDto mockResponseDto = ProductResponseDto.builder()
.id(1L)
.name("겨울 긴팔 티셔츠")
.brandName("테스트 브랜드명")
.info("테스트 인포")
.price(BigDecimal.valueOf(36000))
.image("url")
.colorGroup("밝은")
.tags(List.of("기모", "긴팔T", "겨울", "화이트"))
.stockQuantity(300)
.orderAmountFor30d(0L)
.avgReviewScore(0.0)
.sellerId(1L)
.build();
// given 2
ProductRequestDto requestDto = ProductRequestDto.builder()
.name("겨울 긴팔 티셔츠")
.brandName("테스트 브랜드명")
.info("테스트 인포")
.price(BigDecimal.valueOf(36000))
.image("url")
.colorGroup("밝은")
.tags(List.of("기모", "긴팔T", "겨울", "화이트"))
.stockQuantity(300)
.build();
// given 3 - stubbing
given(sellerProductService.registerProduct(eq(1L),
any(ProductRequestDto.class))).willReturn(
mockResponseDto);
// when
mockMvc.perform(post("/api/seller/product/register").with(csrf())
.contentType(MediaType.APPLICATION_JSON).content(
objectMapper.writeValueAsString(requestDto))).andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("상품 등록이 완료되었습니다."))
.andExpect(jsonPath("$.result.colorGroup").value("밝은"));
// then
verify(sellerProductService, times(1)).registerProduct(eq(1L),
any(ProductRequestDto.class));
}
}
< 코드 세부 설명 >
** 모의 객체의 메서드 호출을 정의하거나 검증할때 인자값을 일반값 또는 Matcher 둘 중 한가지로 통일해야한다. ( 위 코드에서는 any(), eq()를 통해 Matcher로 통일 )
컨트롤러 테스트는 단순히 로직의 성공 여부를 넘어 보안 필터링과 권한 제어가 의도대로 작동하는지 검증하는 과정이 포함되어야 한다. 특히 @AuthenticationPrincipal을 통해 유저 정보를 참조하는 API라면 실제와 동일한 타입의 인증 객체를 시큐리티 컨텍스트에 주입하는 과정이 필수적이다.
컨트롤러 테스트를 위해 @Import(SecurityConfig.class)로 시큐리티 설정을 불러올 때 해당 설정이 의존하는 JwtTokenProvider와 UserDetailsService 빈도 함께 준비되어야 한다. JwtTokenProvider는 실제 로직을 활용하기 위해 직접 임포트하고 DB 조회가 필요한 UserDetailsService는 @MockitoBean으로 선언한다.
이는 시큐리티 내부 로직(필터 체인 구성 등)이 정상적으로 동작할 수 있도록 빈 컨테이너에 필요한 의존성을 가짜 객체로라도 채워 넣어 컨텍스트 로딩 에러를 방지하기 위함이다.
SecurityConfig에는 requestMatchers("/api/seller/**").hasRole("SELLER")가 작성되어있기 때문에 위 테스트 코드에서는 판매자 권한을 가진 유저가 필요하다. 권한을 가진 인증 객체를 생성하기 위해 @WithMockCustomUser(id = 1L, roles = {"판매자"})를 작성해주었다.
특정 권한을 요구하는 API 요청 시에 인증 객체가 없는 경우 아래와 같은 403 Forbidden Error을 맞게된다.

그럼 WithMockCustomUser은 어떻게 인증 객체를 만드는 것인가?
아래와 같이 어노테이션 인터페이스로 작성하여 런타임에만 유지되는 가짜 유저를 만들어준다. 해당 mockUser는 WithMockCustomUserSecurityContextFactory를 통해 생성되어 securityContext에 넣어질 것이다.
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
long id() default 999L;
String name() default "testUser";
String email() default "test@test.com";
String[] roles() default {"구매자"};
}
WithMockCustomUserSecurityContextFactory는 아래와 같이 작성해준다. 어노테이션으로 받았던 정보들을 받아 UserDetailsImpl로서 principal을 만들어주게 되는데 이는 실제 운영환경에서 만들어지는 인증객체와 동일한 타입으로 진행되어야한다. 이후, principal을 담은 Authentication을 생성하고 SecurityContextHolder에 강제로 담는다.
public class WithMockCustomUserSecurityContextFactory implements
WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Set<String> roleNames = Arrays.stream(customUser.roles()).collect(Collectors.toSet());
User mockUser = User.builder()
.id(customUser.id())
.name(customUser.name())
.email(customUser.email())
.password("mockuser123")
.roles(Role.roleFromKorean(roleNames))
.build();
UserDetailsImpl principal = new UserDetailsImpl(mockUser);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
principal,
principal.getPassword(),
principal.getAuthorities()
);
context.setAuthentication(auth);
return context;
}
}
스프링 시큐리티 필터 체인은 컨텍스트에 담겨있는 Role_SELLER 권한을 가진 가짜 인증객체를 확인하고 API 요청을 승인하여 최종적으로 200 OK와 함께 테스트가 성공하게 된다.
이러한 과정을 통해 Spring Security까지 포함한 Controller Layer 테스트가 완료되었다.
.
.
.
다음 포스팅에서는 Service Layer를 테스트하는 방법과 그 예시에 대해 정리한다.