-
컨트롤러를 테스트해보자!프로젝트/게시판 프로젝트 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객체는 주입받을 수 없습니다.
따라서 이를 해결하기 위해서는 @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()이라는 메서드를 사용한다면 서비스가 어떤 일을 수행할지를 지정해줄 수 있습니다.
예를 들어 위의 코드는 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
'프로젝트 > 게시판 프로젝트' 카테고리의 다른 글
로그인 기능을 만들어보자 (0) 2022.05.18 JPA로 Update를 해보자! (0) 2022.05.14 [에러 해결 완료] Type definition error , InvalidDefinitionException (0) 2022.05.12 테스트코드에서는 H2 DB를 사용하자! (0) 2022.05.11 Spring Rest API 예외처리 (feat. 스프링의 기본적인 예외 처리 방법과 Best Practice) (0) 2022.05.10