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);
}
}
참고한 사이트