포스트

Spring Boot Test - Rest Controller 단위 테스트 작성하기

rest controller를 테스트하는 2가지 방법에 대해 정리


코드가 다음과 같은 형태로 구현되어 있을 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*    Rest Controller    */
@RequiredArgsConstructor
@RestController
@RequestMapping("/study")
class StudyApiController {
    
    private final StudyBusiness studyBusiness;
    
    
    @GetMapping("/start/{userId}")
    public ResponseEntity<String> running(@PathVariable long userId) {
        String responseMessage = studyBusiness.startStudy(userId);
        
        return ResponseEntity.ok(responseMessage);
    }
}


/*    Business    */
@RequiredArgsConstructor
@Service
class StudyBusiness {
    
    private final StudyService studyService;


    public boolean startStudy(long userId) {
        studyService.startTimer();
        
        try {
            studyService.todaySchedule(userId);
        
        } catch(UsernameNotFoundException e) {
            studyService.endTimer();
            e.printStackTrace();
            return "존재하지 않는 회원 👻";
        } catch(Exception e) {
            studyService.endTimer();
            e.printStackTrace();
            return "노는 날 🥃🎵";
        }
        
        return "시작! 화이팅 👍";
    }
}


1. WebMVC - MockMvc test

api 실행을 통한 응답 상태에 대한 검증

test 환경에서 어플리케이션이 실행되어 해당 api가 어떤 응답을 내리는지 검증한다.


@WebMvcTest(TargetController.class)를 사용하여 필요한 MVC 관련 bean만 읽어서 mvc 테스트를 진행한다.

MVC 관련 bean
@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver


Service 등 Controller에서 의존하는 하위 레이어는 @MockBean을 통해 Mocking해서 사용한다.


@SpringBootTest 를 이용 할 수도 있지만,
@SpringBootTest를 사용하면 어플리케이션의 모든 bean들을 읽어와서 mvc 테스트를 진행하기 때문에 필요 이상의 리소스를 가져오게 되어 테스트가 느려지는 단점이 있어서 단위 테스트에는 필요한 리소스만 사용하는 @WebMvcTest를 사용하는 것이 더 효율적이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@WebMvcTest(StudyApiController.class) // 옵션 기본값 = value = controllers
class StudyApiControllerTest {

    @Autowired MockMvc mvc;

    @MockBean StudyBusiness studyBusiness;


    @ParameterizedTest
    @ValueSource(strings = {
        "시작! 화이팅 👍",
        "노는 날 🥃🎵",
        "존재하지 않는 회원 👻"
    })
    void running(String doMessage) throws Exception {
        // given
        long userId = 1L;
		
        given(studyBusiness.startStudy()).willReturn(doMessage);
        
        // when
        MvcResult result = mvc.perform(get("/study/start/{userId}", userId)) // api 실행
                              .andExpect(status().isOk()) // api response status 확인
                              .andDo(print()) // console에 response 출력
                              .andReturn(); // response data 반환
                                    
        // then
        String resultMessage = result.getResponse().getContentAsString();
        
        assertEquals(doMessage, resultMessage);
    }
}


Security를 사용 한 경우

Security를 사용한 경우, 관련 bean 일부가 @WebMvcTest 어노테이션으로 읽혀지지 않기 때문에 @MockBean 등을 통해 추가해주어야 한다.

만약 추가할 bean들이 controller 단위 테스트에서 필요 없는 부분이라면 @WebMvcTest 어노테이션의 excludeFilters 옵션을 사용하여 제외 선언을 해주면 된다!

내 경우 권한 별로 api 진입시 응답 값을 확인하고 싶었기 때문에 테스트용 security config 파일을 새로 작성하여 추가해주고 인증 관련 filter 몇 가지를 제외시켜주었다.

사용 한 test 코드 일부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Import(TestSecurityConfig.class)  // 테스트 용 설정파일
@WebMvcTest(  
        controllers = MemberApiController.class,  
        excludeFilters = @ComponentScan.Filter(  // 특정 filter 제외
                type = FilterType.ASSIGNABLE_TYPE, // class를 기준으로 핸들링  
                classes = {  // spring context에 이미 등록된 filter 제와
                        AuthenticationFilter.class,  
                        JwtFilter.class,  
                        SessionCheckFilter.class  
                })  
)  
class MemberApiControllerTest {  
    @MockBean MemberBusiness memberBusiness;  
      
    @Autowired MockMvc mvc;  
    ObjectMapper objectMapper = new ObjectMapper();  
      
      
    @DisplayName("모든 계정 정보 조회")  
    @Nested  
    class FindAllCase {  
        @DisplayName("성공: admin 권한")  
        @WithMockUser(roles = RoleType.TYPE_ADMIN)  
        @Test  
        void findAll_success() throws Exception {  
            given(memberBusiness.findAll()).willReturn(List.of());  
              
            mvc.perform(get("/all"))  
                    .andExpect(status().isOk())  
                    .andExpect(jsonPath("$.status").value(true))  
                    .andDo(print());  
        }  
        
        @DisplayName("실패: member 권한")  
        @Test  
        @WithMockUser(roles = RoleType.TYPE_MEMBER)  
        void findAll_fail_member() throws Exception {  
            given(memberBusiness.findAll()).willReturn(List.of());  
              
            mvc.perform(get("/all"))  
                    .andExpect(status().isBadRequest())  
                    .andExpect(jsonPath("$.status").value(false))  
                    .andDo(print());  
        }


2. Mockito - Mock test

service 코드를 검증하는 것과 같이 테스트할 controller에 대한 mock을 생성한 후 과정을 stubbing하여 검증한다.


이 테스트는 MockMvc를 사용하지 않으므로 spring mvc 호출에 대한 응답은 테스트하지 않는다. 오직 controller의 핸들러 메서드가 실행되었을 때를 예상하고 동작 과정을 검증하는 방법이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ExtendsWith(MockitoExtention.class)
class StudyApiControllerTest {

    @InjectMocks StudyApiController studyApiController;
    @Mock StudyBusiness studyBusiness;


    @ParameterizedTest
    @ValueSource(strings = {
        "시작! 화이팅 👍",
        "노는 날 🥃🎵",
        "존재하지 않는 회원 👻"
    })
    void running(String doMessage) {
        // given
        long userId = 1L;
        
        given(studyBusiness.startStudy()).willReturn(doMessage);
        
        // when
        ResponseEntity<String> result = StudyApiController.running(userId);
        
        // then
        String resultMessage = result.getBody();

        assertEquals(doMessage, resultMessage);
    }
}





참고한 사이트

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.