ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 컨트롤러를 테스트해보자!
    프로젝트/게시판 프로젝트 2022. 5. 13. 00:01

    Service나 Repository에 대해 테스트 코드를 작성하는 것은 익숙했습니다.

    하지만 Controller를 테스트하기 위해서는? 어떻게 해야 하는지 감이 잘 오지 않습니다.

     

    직접 Web UI로 하거나 아니면 조금 더 편리하게 Postman을 사용하여 테스트를 진행했습니다.

     

    지금부터는 직접 테스트 코드를 작성해서 Controller를 테스트해보고자 합니다.

     

     

    목표

    • 컨트롤러를 테스트해보자
    • 컨트롤러에서 인자로 받는 @Valid MemberRequestDTO가 제대로 검증되는지 테스트해보자

    Controller Test

    다음은 현재 Controller의 코드입니다.

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/members")
    public class MemberController {
    
        private final MemberService memberService;
    
        @GetMapping
        public String helloMessage() {
            return "ok";
        }
    
        @PostMapping
        public ResponseEntity<?> registerMember(@Valid @RequestBody MemberRequestDTO memberRequestDTO) {
    
            if (memberService.validateIsDuplicate(memberRequestDTO)) {
                return new ResponseEntity<String>("Duplicated", HttpStatus.CONFLICT);
            }
            memberService.join(memberRequestDTO.toEntity());
            return new ResponseEntity<String>("회원가입 완료", HttpStatus.CREATED);
        }
    
    }

    memberRequestDTO를 받아서 회원이 중복되는지 검증하고 중복되지 않는다면 회원가입을 진행합니다.

     

    다음은 MemberRequestDTO의 코드입니다.

    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder
    @Getter
    public class MemberRequestDTO {
        //@NotNull : Null만 허용하지 않음 "", " "허용
        //@NotEmpty : null, "" 허용하지 않음 " "허용
        //@NotBlank : null, ""," "허용하지 않음
        //@Pattern : 지정된 패턴만 입력하게 하여 휴대폰 번호 폼에서 이상한 값들이 요청되는 것을 방지합니다.
    
        @NotBlank(message = "아이디를 입력해주세요.")
        @Size(min = 5, max = 20, message = "아이디는 5자 이상 20자 이하로 입력해주세요.")
        private String userId;
    
        @NotBlank(message = "비밀번호를 입력해주세요.")
        @Size(min = 8, message = "비밀번호를 8자 이상으로 입력해주세요.")
        private String password;
    
        @NotBlank(message = "별명을 입력해주세요.")
        @Size(max = 20, message = "별명을 20자 이하로 입력해주세요.")
        private String nickName;
    
        @NotBlank(message = "이름을 입력해주세요.")
        private String name;
    
        @NotBlank(message = "휴대전화번호를 입력해주세요.")
        @Pattern(regexp = "(01[016789])(\\d{3,4})(\\d{4})", message = "올바른 휴대폰 번호를 입력해주세요.")
        private String phoneNumber;
    
        Address address;
    
        public Member toEntity() {
            return Member.builder().userId(userId).password(password).nickName(nickName).name(name).phoneNumber(phoneNumber).address(address).build();
        }
    
    }

    javax.validation 라이브러리를 사용하여 필드 값의 유효성들을 검증해줍니다.

     

     

    다음은 MemberControllerTest 코드입니다.

    package anthill.Anthill.controller;
    
    import anthill.Anthill.dto.member.MemberRequestDTO;
    import anthill.Anthill.service.MemberService;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.http.MediaType;
    import org.springframework.test.web.servlet.MockMvc;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    
    
    
    @WebMvcTest
    class MemberControllerTest {
    
        @Autowired
        private MockMvc mvc;
    
        @MockBean
        private MemberService memberService;
    
        @Test
        @DisplayName("Hello Test")
        public void returnOkMessage() throws Exception {
            //given
            String ok = "ok";
    
            //when
            mvc.perform(get("/members"))
                    //then
                    .andExpect(status().isOk())
                    .andExpect(content().string(ok));
        }
    
        @Test
        @DisplayName("memberRequestDTO의 입력값들이 유효하지 않을때")
        public void memberPostDataInValidateTest() throws Exception {
            //given
            MemberRequestDTO memberRequestDTO = MemberRequestDTO.builder().build();
            String body = (new ObjectMapper()).writeValueAsString(memberRequestDTO);
    
            //when
            mvc.perform(post("/members")
                            .content(body)
                            .contentType(MediaType.APPLICATION_JSON)
                            .accept(MediaType.APPLICATION_JSON)
                    )
                    //then
                    .andExpect(status().isBadRequest());
        }
    
    
    
    }

    @WebMvcTest란?

    Controller를 테스트하기 위한 어노테이션입니다.

    Controller가 정상적으로 작동하는지 테스트하는 것이기 때문에 Web과 관련된 의존성만을 가지고 옵니다.

     

    관련된 설정들

    @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver

     

    만약 다음처럼 Controller가 Service를 의존하고 있는 상황이라면 Service객체는 주입받을 수 없습니다.

    Controller가 Service를 의존하고 있는 상황

    따라서 이를 해결하기 위해서는 @MockBean을 사용하여 Mock 객체를 주입해주어야 합니다.

    Mock이란 모조품이란 뜻을 가진 영어단어이기 때문에 Mock 객체는 가짜 객체라고 생각하면 좋습니다.

     

    가짜 객체이기 때문에 실제 행위를 하는 객체는 아니며 기존에 정해진 동작을 수행하지도 않습니다.

    만약 given 메서드를 사용한다면 가짜 객체가 원하는 행위를 할 수 있도록 정의할 수 있습니다.

     

    @MockBean의 역할

    Annotation that can be used to add mocks to a Spring ApplicationContext. Can be used as a class level annotation or on fields in either @Configuration classes, or test classes that are @RunWith the SpringRunner.

    @RunWith SpringRunner 또는 @Configuration 클래스의 필드 또는 클래스 수준 주석으로 사용할 수 있으며 SpringApplicationContext에 mock을 추가하는 데 사용할 수 있는 주석입니다.

     

    하지만 @WebMvcTest 어노테이션을 들어가 보면 @RunWith이 존재하지 않습니다.

    바로 JUnit 5를 사용하면 @RunWith대신에 @ExtendWith를 사용하기 때문이고 ExtendWith는 위의 어노테이션에서 확인할 수 있습니다.

     

    MockMvc란?

    컨트롤러를 테스트하기 위해 실제 애플리케이션 서버에 배포하지 않고 테스트용으로 시물레이션 하도록 MVC 환경을 만들어주는 클래스입니다.

     

    mvc.perform()

    MockMvc가 제공하는 메서드로 브라우저에서 서버로 URL을 요청하듯 컨트롤러를 실행하여 테스트할 수 있습니다.

     

    ObjectMapper란?

    Json 콘텐츠를 Java 객체로 deserialization 하거나 Java 객체를 JSON으로 serialization 할 때 사용하는 Jackson 라이브러리 클래스입니다.

     

    이때 Java 객체를 JSON으로 serialization 하고 String 형태로 넘겨주기 위해서 ObjectMapper의 writeValueAsString 메서드를 사용합니다.

     

    여기서 한 단계 더 나아갈 수 있습니다.

    우리는 Controller에서 MemberService를 의존하고 있었기 때문에 이를 @MockBean을 활용하여 가짜 서비스 객체를 사용했습니다.

     

    여기서 바로 given()이라는 메서드를 사용한다면 서비스가 어떤 일을 수행할지를 지정해줄 수 있습니다.

     

    given() 메서드 사용

    예를 들어 위의 코드는 memberService에 validateIsDuplicate 메서드를 호출할 경우에는 false를 반환할 것이다

    라고 지정해 주는 것입니다.

     

    그러면 다음과 같이 회원 중복이 발생하지 않았을 때는 정상적으로 201 상태 코드가 반환되는 테스트를 작성할 수 있습니다.

     

    다음은 MemberControllerTest의 memberNonDuplicateTest 메서드입니다.

      @Test
        @DisplayName("회원 중복 발생안됬을 시 201 상태코드 반환")
        public void memberNonDuplicateTest() throws Exception {
            //given
            MemberRequestDTO memberRequestDTO = MemberRequestDTO.builder().userId("junwooKim").name("KIM").nickName("junuuu").password("123456789").phoneNumber("01012345678").build();
            String body = (new ObjectMapper()).writeValueAsString(memberRequestDTO);
            boolean duplicateResult = false;
            given(memberService.validateIsDuplicate(any())).willReturn(duplicateResult);
    
            //when
            ResultActions resultActions = mvc.perform(post("/members")
                    .content(body)
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
            );
    
            //then
            resultActions
                    .andExpect(status().isCreated());
        }

     

    한 가지 팁으로 여기서 유사한 라이브러리 2개가 존재합니다.

    • when.thenReturn()을 사용하는 org.mockito.Mockito
    • given().willReturn()을 사용하는 org.mockito.BDDMockito

    BDDMockito는 Mockito를 상속한 클래스이며 동작, 사용방법이 Mockito와 거의 차이가 없습니다.

    BDDMockito는 시나리오에 맞게 테스트 코드가 읽힐 수 있도록 도와주는 (이름을 변경한) 프레임워크입니다.

     

    즉, given when then 패턴에 맞출 수 있도록 가독성을 높여주는 BDDMockito 사용이 권장됩니다.

     

    결론

    @WebMvcTest를 통해 Controller를 테스트할 수 있다.

    이때 만약 Controller가 Service를 의존하고 있다면 @MockBean으로 가짜 객체를 주입해주어야 한다.

    필요에 따라 given 메서드를 사용하여 의존하는 객체의 행위를 가정할 수 있다.

    이후에는 perform 메서드를 사용하여 URL을 요청하듯 테스트를 실행할 수 있다.

    필요에 따라 ObjectMapper를 사용하여 객체를 넘겨준다.

    .andExpect 메서드를 통해서 결과를 검증한다

     

     

     

     

     

    출처

    https://dadadamarine.github.io/java/spring/2019/03/16/spring-boot-validation.html

     

    Push Stone's blog

    테스트는, 자신을 작성한 만큼만 기능을 보장해준다. 그러니까 테스트를 작성하는 일은 각 기능에 대한 보증서를 모으는 작업이다.

    dadadamarine.github.io

    https://velog.io/@lxxjn0/Mockito%EC%99%80-BDDMockito%EB%8A%94-%EB%AD%90%EA%B0%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C

     

    Mockito와 BDDMockito는 뭐가 다를까?

    이 글은 우아한테크코스 리뷰 페이지에 함께 게시된 글입니다. 해당 게시글은 JUnit5.x를 기준으로 작성되었습니다. 우아한테크코스 레벨2 미션 중에 의문이 생긴 적이 있었다.

    velog.io

     

    댓글

Designed by Tistory.