본문 바로가기

Proj/Tripot

스프링부트 테스트코드 작성하기: Controller

1. 개요

이전 포스팅에서 repository, service 계층의 단위 테스트를 작성해보았다. 이번에는 controller 계층의 단위테스트를 작성해보고자 한다.

2. Controller

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)     //JPA 관련 빈들을 mock으로 등록
class MemberControllerTest {


    @Autowired
    MockMvc mockMvc;

    @MockBean
    MemberService memberService;

    @Autowired
    ObjectMapper objectMapper;


    @Test
    @DisplayName("회원 활성화 완료 응답이 반환되어야 함")
    @WithMockCustomUser
    void activeMember() throws Exception {

        //given
        ActivateMemberDto activateMemberDto = new ActivateMemberDto("updatenick", "강원");

        String content = objectMapper.writeValueAsString(activateMemberDto);

        //when
        ResultActions actions = mockMvc.perform(
                patch("/api/v1/members/activate")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
                        .with(csrf())
        );


        //then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.customCode").value("MEMBER-SUCCESS-001"))
                .andExpect(jsonPath("$.customMessage").value("회원 활성화 성공"))
                .andExpect(jsonPath("$.status").value(true))
                .andExpect(jsonPath("$.data").value(nullValue()));
                
        
        ...
    }

 

위 코드는 회원의 추가 정보를 입력받는 기능의 컨트롤러 계층 테스트코드이다. 하나씩 살펴보자.

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)     //JPA 관련 빈들을 mock으로 등록
  • @WebMvcTest(MemberController.class): 웹 계층에 관련된 컴포넌트들만을 로드한다.
  • @MockBean(JpaMetamodelMappingContext.class): 본 프로젝트에서는 Auditing 기능을 사용한다. 이에 따라 본 애노테이션이 없으면 코드가 동작하지 않는다.
    @Autowired
    MockMvc mockMvc;

    @MockBean
    MemberService memberService;

    @Autowired
    ObjectMapper objectMapper;

MemberController는 MemberService를 의존받으므로 해당 클래스를 MockBean으로 등록해준다. 또, 요청을 보내기 위해 MockMvc를, DTO를 요청으로 만들기 위해 ObjectMapper를 실제 빈으로 등록해주었다.

 

    @Test
    @DisplayName("회원 활성화 완료 응답이 반환되어야 함")
    @WithMockCustomUser
  • @Test: 해당 메서드는 테스트코드임을 명시한다.
  • @DisplayName: 해당 테스트의 이름을 명시한다.
  • @WithMockCustomUser: 본 프로젝트는 스프링 시큐리티를 사용하고 있기 때문에 해당 요청에 대해서 가짜 사용자를 만들어 SecurityContext에 넣어주어야 한다. 자세한 내용은 후술
        //given
        ActivateMemberDto activateMemberDto = new ActivateMemberDto("updatenick", "강원");

        String content = objectMapper.writeValueAsString(activateMemberDto);

given 파트이다. 임의의 dto를 하나 생성하고, objectmapper를 사용하여 json 형식으로 바꿔준다.

        //when
        ResultActions actions = mockMvc.perform(
                patch("/api/v1/members/activate")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
                        .with(csrf())
        );

when 파트이다. 추가 정보를 입력받는 요청을 보낸다. 해당 경로에 patch method로 json 형식의 body(content)를 담아 mock csrf를 붙여 요청을 보낸다.(시큐리티 관련 설정은 추후 적용예정)

        //then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.customCode").value("MEMBER-SUCCESS-001"))
                .andExpect(jsonPath("$.customMessage").value("회원 활성화 성공"))
                .andExpect(jsonPath("$.status").value(true))
                .andExpect(jsonPath("$.data").value(nullValue()));

then 파트이다. 해당 요청에 대한 응답은 200에, json의 각 요소에 대해 기대하는 값이 나오는 지 검증할 수 있다. 아래 링크에서 자세한 사용법을 알 수 있다.

https://github.com/json-path/JsonPath

 

GitHub - json-path/JsonPath: Java JsonPath implementation

Java JsonPath implementation. Contribute to json-path/JsonPath development by creating an account on GitHub.

github.com

3. @WithCustomMockUser

회원을 활성화하기 위해서는 앞서 언급했듯이 UserDetails를 SecurityContext에 사전에 담는 작업이 필요하다. 이를 가능케 하는 방법은 세 가지가 있다.

  • @WithMockUser
  • @WithUserDetails
  • @WithSecurityContext

해당 프로젝트는 JWT를 사용함에, 위의 작업을 커스텀했으므로 3번째 방법을 사용하기로 했다. 우선 애노테이션을 하나 만들어준다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

    long id() default 2L;
    String nickname() default "테스트닉";
    String username() default "username";
    MemberRole role() default MemberRole.USER;
    SignUpType signUpType() default SignUpType.KAKAO;
    String profileImage() default "s3.com/testProfile";
    String recommendLocation() default "서울";
    MemberStatus status() default MemberStatus.ACTIVE;
;

}

해당 애노테이션에 사용자의 정보를 담고, @WithSecurityContext를 담는다. 그리고 securitycontext에 정보를 담을 클래스를 factory로 지정해준다.

public class WithMockCustomUserSecurityContextFactory implements
        WithSecurityContextFactory<WithMockCustomUser> {


    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser mockCustomUser) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        Member member = Member.builder()
                .id(mockCustomUser.id())
                .nickname(mockCustomUser.nickname())
                .username(mockCustomUser.username())
                .role(mockCustomUser.role())
                .signUpType(mockCustomUser.signUpType())
                .profileImage(mockCustomUser.profileImage())
                .recommendLocation(mockCustomUser.recommendLocation())
                .status(mockCustomUser.status())
                .build();

        UserPrincipal principal = new UserPrincipal(member);

        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());

        context.setAuthentication(authToken);
        return context;
    }
}

앞서 적은 회원 정보를 기반으로 UserPrincipal(extends UserDetails)를 만들어 SecurityContext에 넣어준다.

 UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

넣은 UserDetails는 위와 같이 다시 꺼내어 사용할 수 있다.

 

3. 여담

현재 설정이 덜 되어 각 요청에 .with(csrf())를 넣어주었다. 이후에는 이를 빼고, SecurityConfig를 적용해볼 것이다.