Dev_Henry

[Spring] mock을 이용한 mvc 단위테스트 - 3. controller 본문

Web/Spring

[Spring] mock을 이용한 mvc 단위테스트 - 3. controller

데브헨리 2023. 7. 25. 00:12
728x90

 

이어서 controller의 테스트 코드를 작성해 보겠다.

 

  • memberAccountController
    @ApiOperation(value = "회원가입")
    @PostMapping("/join")
    public ResponseEntity<ResultResponse> joinPOST(@Valid @RequestBody MemberJoinDTO memberJoinDTO){
        memberService.join(memberJoinDTO);
        //return ResponseEntity.ok(ResultResponse.of(REGISTER_SUCCESS, true));
        return ResponseEntity.status(HttpStatus.CREATED).body(ResultResponse.of(REGISTER_SUCCESS, true));
    }
 

 

테스트 코드를 작성하면서 알게된 것이 있다.

 

기존에 만들어둔 코드들은 요청이 성공하면 모두 응답코드 200을 리턴해줬는데, 이는 restful 하지 못한 코드였다.

rest api를 만들면서 restful다운 것이 뭔지에 관한 글도 많이 읽었고 고민해봤지만 잘 와닿지가 않아서 충분히 restful스럽게 코드를 작성하지 못하고 있었다. 요청을 받을 때 http메서드를 이용하여 요청을 구분하는 방법은 생각하고있었지만, 응답을 줄때 코드까지는 생각하지 못했었다.

http상태코드 중에는 요청이 성공적으로 처리되었으며, 자원이 생성되었음을 나타내는 성공 상태 응답 코드인 201 (created)가 있기 때문에,

회원가입,글 작성 등 리소스를 생성하는 함수는 201 상태코드를 사용해서 응답을 주는게 좋을듯하다.

 


 

mvc컨트롤러를 테스트할때는 @WebMvcTest어노테이션을 사용하는게 일반적이다.

이전까지는 @ExtendWith(MockitoExtension.class)를 사용하여 테스트를 가볍게 만들어 단위테스트를 진행했었기 때문에 처음에는 이부분도 동일하게 모키토만 사용하여 코드를 작성했는데 두가지 방법 모두 보겠다.

 

먼저 @ExtendWith(MockitoExtension.class)를 사용한 테스트코드

@ExtendWith(MockitoExtension.class)
@Slf4j
class MemberAccountControllerTests {
    @InjectMocks
    private MemberAccountController memberAccountController;
    @Mock
    private MemberServiceImpl memberService;
    private MockMvc mockMvc;
    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.standaloneSetup(memberAccountController).build();
    }
    @DisplayName("회원가입 성공")
    @Test
    void joinPOST() throws Exception {
        //given
        MemberJoinDTO memberJoinDTO = joinDTO();
        //when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/account/join")
                .contentType(MediaType.APPLICATION_JSON)
                .content(new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(memberJoinDTO)));
        // then
        verify(memberService).join(any(MemberJoinDTO.class));
        resultActions.andExpect(status().isCreated());
    }
    private MemberJoinDTO joinDTO() {
        return MemberJoinDTO.builder()
                .memberId("testcode999").memberPassword("a1234567").name("테스트")
                .nickName("테스트코드999").birthDay(LocalDate.now())
                .phone("01012345678").height(180).weight(70).gender("M")
                .build();
    }
}
 

이전 테스트코드와 동일하게 mock을 사용해서 테스트 코드를 구성한다.

mvc테스트를 위해서는 mockMvc를 사용하는데 스프링의 테스트 기능은 사용하지 않기때문에 자동주입은 할 수 없고, init()에서 빌더를 통해 생성해준다.

 

mockMvc객체를 통해서 회원가입 요청경로로 dto를 post형식으로 요청한다.

(이때 dto를 json으로 바꾸는 과정에서 localDate타입이 있어서 에러가 발생했었는데 이는

'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' 에 의존성 추가하고 JavaTimeModule()을 등록해주면 해결 가능하다.)

 

verify()를 통해 정상적으로 서비스의 join함수에 dto가 전달되어 실행했는지를 검사하고, 결과가 201코드를 리턴하는지 검사한다.

 

이렇게 controller의 테스트코드를 작성해 볼 수 있었다.


찾아보니 controller단은 웹과 맞닿아서 요청을 처리하는 레이어로, 모든 외부요소를 차단한 단위테스트는 의미가 크지않고, 슬라이스 테스트로 진행한다는 글이 있었다.

 

@WebMvcTest 코드의 내부를 보면 @ExtendWith(SpringExtension.class)가 붙어있어 spring testContext를 사용한다.

또 WebMvcTypeExcludeFilter 내부로 들어가보면 controller 레이어까지만 테스트 한다는 것을 알 수 있다.

 

이러한 점들을 생각하면서 코드를 작성하는 과정에 두 가지 문제를 만났다.

 

첫 번째 문제는

이런 에러였다. controller내부에서는 딱히 entity를 사용하는 부분이 없어서 상관없을 거라고 생각했는데 jpa를 쓴다면 설정해줘야하나 보다.

@MockBean(JpaMetamodelMappingContext.class)를 추가하여 해결했다.

 

두번째 문제는 결과값이 403코드가 나왔다.

앞서 말했듯 @WebMvcTest는 단위테스트보다 넓고, 통합테스트보다는 좁은 범위인 슬라이스 테스트를 하는 방법이다.

바로 위에서 본 WebMvcTypeExcludeFilter의 코드를 조금 더 보면

이런 녀석들도 포함한다.

단순히 controller만이 아니라, 그전에 각종 설정파일과 security, filter까지 포함하는 것이었다.(여기서 또 문제가 생기는데 이때 불러오는 설정파일은, 예를들어 securityConfig파일은 내가 만든 설정파일이 아닌 자동으로 구성되는 파일이라고 한다..)

생각해보니 권한 검사같은 경우 api를 요청할때 필요한 것이니.. 이래서 controller의 테스트는 단위테스트가 아닌 슬라이스 테스트로 작성하는구나 싶었다.

 

아무튼 이를 해결하기 위해서는 유저또한 mock객체를 만들어서 처리한다.

테스트 위에 @WithMockUser(roles = "USER")를 붙여주자. 그럼 또 403에러가 발생한다. 로그를 살펴보자.

내가 만든 시큐리티 설정파일에는 csrf.disable 을 적용시켰지만 자동으로 구성되는 설정파일에는 없나보다.. mockMvc로 요청할 때 .with(csrf())도 추가해주도록 하자. 모든 준비가 끝나고 테스트가 가능해진다.

 

  • memberAccountControllerTests
@WebMvcTest(MemberAccountController.class)
@MockBean(JpaMetamodelMappingContext.class)
@Slf4j
class MemberAccountControllerTests1 {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private MemberServiceImpl memberService;
    @MockBean
    private OAuth2MemberService oAuth2MemberService;

    @DisplayName("회원가입 성공")
    @Test
    @WithMockUser(roles = "USER")
    void joinPOST() throws Exception {
        //given
        MemberJoinDTO memberJoinDTO = joinDTO();
        //when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/account/join").with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content(new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(memberJoinDTO)));
        // then
        verify(memberService).join(any(MemberJoinDTO.class));
        resultActions.andExpect(status().isCreated());
    }
..
}
 

그리고 webMvcTest는 스프링의 기능을 사용할 수 있기때문에 자동주입이 가능하다.

mockMvc 객체도 autowired해주고, 다른 필요한 녀석들은 mockBean으로 등록해주면 알아서 주입된다.

 

 

하지만 문득 든 생각은 지금 테스트 코드를 작성중인 memberAccountController는 회원가입과 관련된 컨트롤러이다. 즉 권한이 없이 접근하는 컨트롤러이고 당연히 내가만든 시큐리티 설정파일에는 permitAll() 처리 되어있다.

그리고 자동 설정해주는 시큐리티config는 가장 간단한 시큐리티 적용코드만 있다.

아무튼 이 컨트롤러를 user권한을 주고 테스트하는건 큰 영향은 없겠지만 다소 목적과 맞지않기 때문에 수정이 필요하다.

해결방법으로는 시큐리티 설정을 제외시키는 방법과, TestConfiguration을 작성하는 방법이 있다.

테스트 클래스 위에 @AutoConfigureMockMvc(addFilters = false)를 붙여주면 내가만든 필터를 포함해서 관련 필터를 제외시켜 mockUser를 사용하지 않고도 진행이 가능했다.

 


 

이렇게 최종적으로 controller를 테스트하는 두가지 방법을 경험해보았고 성공적으로 테스트를 진행할 수 있었다.

 

한 개발자 유튜버가 회사에서 코드를 작성할때 테스트 코드를 작성하는 시간이 70%이상이고 테스트 코드를 먼저 작성한 후에 구현을 시작한다는 얘기를 들은 적이 있었다. 아무래도 그때는 공감하지 못했었는데, 이번 경험으로 테스트 코드의 목적에 대해 체감하고 생각하게 되었다.

처음 목적이었던 리팩토링, 유지보수를 쉽게 하기 위함도 물론 있지만 확실히 테스트 코드가 전체적인 설계도 역할이 될 수 있다고 느꼈다.

단위 테스트를 위해 기능들을 분리하여 생각하고 작성하면서 자연스럽게 의존성이 적고, 객체지향적인 설계가 가능하게 되는 것이다.

다음 프로젝트를 할때는 테스트 주도 개발방식을 도입해서 진행해봐야겠다.

 

그런데 또 의문이 생겼다...ㅋㅋ

 

학습을 위해 두가지 방법을 모두 구현해봤지만, 시큐리티관련 부분은 따로 테스트코드를 작성하는게 좋을것 같아서 controller의 테스트 코드는 그냥 첫번째 방법을 사용하려고 했었다.

왜? 통합테스트가 아닌 단위 테스트를 사용하는 이유와 같다. 슬라이스 테스트인 @WebMvcTest보다 @ExtendWith로 모키토만 사용하는 것이 테스트를 위해 준비하는 범위가 좁고 가벼우니까. 즉 속도가 빠르다고 생각했다.

 

그런데 이건 뭐지.

단위 테스트 방법

 

슬라이스 테스트 방법

 

왜 슬라이스 테스트 방법이 훨씬 빠른거지?

 

챗 gpt의 대답도 내가 알고있던 내용과 같다. 일반적으로 모키토만 포함하는것이 스프링을 포함하는 것 보다 가볍고 빠르다.

 

근데 왜 이런 결과가 나온걸까.. 이런 질문에 gpt는 아래와 같이 대답했다.

이게 정확한 대답인지, 맞다면 이 중 무엇이 원인인지는 아직 잘 모르겠다. 이런 내용은 앞으로 더 공부를 하면서 알아가야 할 것 같다.

조금 다른 얘기지만 처음엔 chat gpt가 잘못된 대답도 자주 나오고 아직은 별로라고 생각했는데, 요즘 갈수록 gpt에 질문을 자주 하는 것 같다.

물론 이게 맞는 대답인지 검증하는 과정이 필요하지만 확실히 찾아보는 시간과 범위를 줄여주는 데는 도움이 되는 듯하다.

 

그리고 공부를 하면 할수록 모르는 게 더 많이 생긴다는 것도 계속 느끼는 중이다...

 

728x90
반응형