컨트롤러 역시 다른 계층을 개발할 때와 마찬가지로 테스트 코드를 먼저 작성하도록 하자.
컨트롤러는 함수 호출이 아닌 API 호출을 통해 요청을 받고 응답을 처리해야 하며, 메세지 컨버팅 등과 같은 작업이 필요하다. 그러므로 MockMvc라는 클래스를 이용해야 하는데, 이에 대한 초기화를 하는 테스트부터 작성하도록 하자.
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
public class MembershipControllerTest {
private MembershipController target;
private MockMvc mockMvc;
private Gson gson;
@Test
@DisplayName("mockMVC가 Null이 아님")
void testMock() throws Exception{
assertThat(target).isNotNull();
assertThat(mockMvc).isNotNull();
}
}
위와 같이 테스트를 작성하고 실행하면 실패합니다.이를 해결하기 위해 다음과 같이 테스트를 수정해야 합니다.
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
public class MembershipControllerTest {
@InjectMocks
private MembershipController target;
private MockMvc mockMvc;
private Gson gson;
@Test
@DisplayName("mockMVC가 Null이 아님")
void testMock() throws Exception{
mockMvc = MockMvcBuilders.standaloneSetup(target).build();
assertThat(target).isNotNull();
assertThat(mockMvc).isNotNull();
}
//MockMvc를 사용하기 위해서는 Mockmvc타입의 변수가 필요합니다.
//변수를 초기화 해줘야 사용할 수 있습니다.
//스태틱 클래스인 MockMvcBuilders를 이용하여 초기화
//@Controller 어노테이션이 붙은 클래스를 등록하고 SpringMVC인프라를 설정하여 MockMvcfmf aksemqslek.
}
이제 테스트를 해보면 통과하고 초록막대가 보입니다.이제 리팩토링을 해봅니다.
위의 테스트 코드를 보면 MockMvc는 다른 테스트에서도 사용이 될것으로 보입니다. 그러므로 각각의 테스트마다 독립적으로 객체를 만들어 좋으면 좋은데, 이를 위해 각각의 테스트가 실행되기 전에 초기화를 도와주는 @BeforEach를 사용해줍니다.
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
public class MembershipControllerTest {
@InjectMocks
private MembershipController target;
private MockMvc mockMvc;
private Gson gson;
@BeforeEach
public void init(){
mockMvc = MockMvcBuilders.standaloneSetup(target).build();
}
@Test
@DisplayName("mockMVC가 Null이 아님")
void testMock() throws Exception{
assertThat(target).isNotNull();
assertThat(mockMvc).isNotNull();
}
}
이렇게 되면 mockMvc가 Null인지 검사하는 테스트는 더이상 필요가 없습니다.그러므로 이 테스트는 제거하고 init 함수만 남긴 채로 API개발에 들어가도록 합니다.
참고로 컨트롤러 테스트를 위해 @WebMvcTest를 이용할수도 있습니다. 하지만 @WebMvcTest를 이용하면 테스트 속도가 느리므로 직접 MockMvc를 만들어주도록 합니다.
API 호출 시에 발생하는 여러 개의 실패 케이스 중에서 사용자 식별값이 헤더에 없어서 실패하는 케이스부터 작성해보도록 해봅니다.
@Test
@DisplayName("멤버십등록실패_사용자식별값이 헤더에 없습니다.") throws Exception{
//given
final String url = "/api/v1/memberships";
//when
mockMvc.perform(
MockMvcRequestBuilders.post(url)
.content(gson.toJson(membershipRequest(10000, MembershipType.NAVER)))
.contentType(MediaType.APPLICATION_JSON)
);
//then
resultActions.andExpect(status().isBadRequest());
}
private MembershipRequest membershipRequest(final Integer point,final MembershipType membershipType){
return MembershipRequest.builder()
.point(point)
.membershipType(membershipType)
.build();
}
}
Gson도 다른 API 호출마다 사용될 것 같으니 @BeforeEach에 넣어둡니다.
그리고 아직 테스트 코드에는 컴파일 에러가 남아있습니다. 컴파일 에러를 해결하기 위해 다음과 같이 클래스들을 추가해줍니다.
@Getter
@Builder
@NoArgsConstructor(force = true)
@RequiredArgsConstructor
public class MembershipRequest {
private final Integer point;
private final MembershipType membershipType;
}
@RestController
public class MembershipController {
}
그리고 테스트를 실행하면 아직 API를 개발하지 않았으므로 당연히 404 NOT FOUND가 발생합니다.멤버십 추가에 대한 프로덕션 코드를 다음과 같이 작성할 수 있습니다.
@RestController
public class MembershipController {
@PostMapping("/api/vi/memberships")
public ResponseEntity<MembershipResponse>addMembership(
@RequestHeader(USER_ID_HEADER) final String userId,
@RequestBody final MembershipRequest membershipRequest){
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MembershipConstants {
public final static String USER_ID_HEADER ="X-USER-ID";
}
그리고 사용자 식별값이 헤더에 없어 실패하는 테스트 케이스는 통과를 하게됩니다.
이번에는 사용자가 보낸 데이터가 음수거나 Null인 경우 또는 멤버십 타입이 null인 경우에 실패하는 테스트 코드를 작성해보도록합니다.
@Test
@DisplayName("멤버십등록실패_사용자식별값이 헤더에 없습니다.")
void testFailed1()throws Exception{
//given
final String url = "/api/v1/memberships";
//when
ResultActions resultActions = mockMvc.perform(
MockMvcRequestBuilders.post(url)
.header(USER_ID_HEADER,"12345")
.content(gson.toJson(membershipRequest(10000, MembershipType.NAVER)))
.contentType(MediaType.APPLICATION_JSON)
);
//then
resultActions.andExpect(status().isBadRequest());
}
@Test
@DisplayName("멤버십등록실패_포인트가음수")
public void testFailed2()throws Exception{
//given
final String url = "/api/v1/memberships";
//when
final ResultActions resultActions = mockMvc.perform(
MockMvcRequestBuilders.post(url)
.header(USER_ID_HEADER, "12345")
.content(gson.toJson(membershipRequest(-1, MembershipType.NAVER)))
.contentType(MediaType.APPLICATION_JSON)
);
//then
resultActions.andExpect(status().isBadRequest());
}
@Test
@DisplayName("멤버십등록실패_멤버십종류가 Null")
void testFailed3()throws Exception{
//given
final String url = "/api/v1/memberships";
//when
final ResultActions resultActions = mockMvc.perform(
MockMvcRequestBuilders.post(url)
.header(USER_ID_HEADER, "12345")
.content(gson.toJson(membershipRequest(100000, null)))
.contentType(MediaType.APPLICATION_JSON)
);
//then
resultActions.andExpect(status().isBadRequest());
}
원래라면 각각의 테스트가 통과된 후에 다음의 테스트를 작성해야 한다. 하지만 위의 3가지 테스트는 너무 유사하기 때문에 한번에 작성을 진행하였다.
그리고 테스트를 실행하면 실패했다고 나온다. 왜냐하면 해당 값들에 대한 유효성 검사가 진행되지 않았기 때문이다. 테스트를 통과하도록 유효성 검사를 진행해야 하는데, 이 경우에는 Javax의 Validation 기능인 @Valid을 이용하도록 하자.
Javax의 Validation을 통해 유효성 검증을 진행하도록 다음과 같이 프로덕션 코드를 수정할 수 있다.