단위 테스트 작성 기본 구조
1개의 단위 테스트를 3단계로 나눈다.
- Given : 어떠한 데이터가 주어졌을 때
- When : 어떠한 함수를 실행하면
- Then : 어떠한 결과가 나와야 함
기본 개념 요약
Mockito
- 개발자가 동작을 직접 제어할 수 있는 가짜 객체를 지원하는 테스트 프레임워크
- 가짜 객체를 주입시켜 객체 간의 의존성을 제거
Mock 객체 의존성 주입 Annotation
- @Mock: 가짜 객체를 만들어 반환해주는 어노테이션
- @Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
- @InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션
Example)
UserController에 대한 단위 테스트를 작성할 때 @Mock을 통해 가짜 UserService 객체를 만들고 @InjectMocks를 통해 컨트롤러 내에 주입한다.
stub 메소드
의존성 있는 객체는 가짜 객체를 주입해서 어떤 결과를 반환하라고 정해진 답변을 준비시켜야 한다.
- doReturn(): 가짜 객체가 특정한 값을 반환해야 하는 경우
- doNothing(): 가짜 객체가 아무 것도 반환하지 않는 경우(void)
- doThrow(): 가짜 객체가 예외를 발생시키는 경우
Mockito와 Junit 결합
@ExtendWith(MockitoExtension.class)
MockMVC
- 스프링 내에서 HTTP 호출
Layer별 단위 테스트 예제
Repository 단위 테스트
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@DisplayName("사용자 추가")
@Test
void 사용자추가(){
// given
final User user = SignUpRequest.builder()
.name("최규현")
.email("rbgus200@@naver.com")
.nickName("규규")
.phoneModel("")
.picture("")
.build().toEntity();
// when
final User userSaved = userRepository.save(user);
// then
assertThat(userSaved.getUserId()).isNotNull();
assertThat(userSaved.getProfile().getName()).isEqualTo("최규현");
assertThat(userSaved.getProfile().getEmail()).isEqualTo("rbgus200@@naver.com");
}
}
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
Default로 설정되는 h2 memory DB가 아닌 현재 로컬에서 설정된 DB를 사용하겠다!
@DataJpaTest
Jpa Test 에 포커싱이 맞춰진 테스트 Annotation
Summary
- JPARepository에서 기본적으로 제공하는 method는 테스트 하지 않습니다. 주로 @Query로 작성된 JPQL 등으로 커스텀하게 추가된 메소드만이 테스트 대상입니다.
Service 단위 테스트
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
private SignUpRequest getSignUpRequest() {
return SignUpRequest.builder()
.name("최규현")
.email("rbgus200@@naver.com")
.nickName("규규")
.phoneModel("")
.picture("")
.build();
}
private User getUser() {
return getSignUpRequest().toEntity();
}
@Test
@DisplayName("회원가입_실패_이메일중복")
void UserServiceTest(){
// given
doReturn(true).when(userRepository).existsByProfileEmail(any(String.class));
// when
EmailExistsException exception = assertThrows(EmailExistsException.class, () -> userService.signUp(getSignUpRequest()));
// then
assertThat(exception.getResultCode()).isEqualTo(ResultCode.DUPLICATE_EMAIL);
}
@Test
@DisplayName("회원가입_성공")
void 회원가입_성공() {
// given
doReturn(false).when(userRepository).existsByProfileEmail(any(String.class));
doReturn(getUser()).when(userRepository).save(any(User.class));
// when
final User newUser = userService.signUp(getSignUpRequest());
// then
assertThat(newUser).isNotNull();
assertThat(newUser.getProfile().getName()).isEqualTo("최규현");
}
}
@ExtendWith
Mockito와 Junit 결합
@InjectMocks
Mocking한 객체들을 주입해주겠다!
@Mock
Mocking해서 가짜 객체를 만들겠다!
doReturn().when().~
Mocking한 객체가 ~메소드 사용했을 때 return 해줄 값을 명시적으로 정해줌
any(A.class)
A 타입에 해당하는 객체를 만들어 임의로 넣어주겠다!
Controller 단위 테스트
@ExtendWith(MockitoExtension.class)
class UserApiTest {
@InjectMocks
private UserApi userApi;
@Mock
private UserService userService;
private MockMvc mockMvc;
private Gson gson;
@BeforeEach
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(userApi)
.setControllerAdvice(new GlobalExceptionHandler())
.build();
gson = new Gson();
}
private SignUpRequest getSignUpRequest() {
return SignUpRequest.builder()
.name("최규현")
.email("rbgus2002@naver.com")
.nickName("규규")
.phoneModel("")
.picture("")
.build();
}
@Test
@DisplayName("회원가입_실패_이메일형식X")
void 회원가입_실패_이메일형식X() throws Exception {
// given
final String url = "/user";
// when
final ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post(url)
.content(gson.toJson(SignUpRequest.builder()
.name("최규현")
.email("rbgus2002")
.nickName("규규")
.phoneModel("")
.picture("")
.build()))
.contentType(MediaType.APPLICATION_JSON)
);
// then
resultActions.andExpect(status().isBadRequest());
}
@Test
@DisplayName("")
void 회원가입_실패_이메일중복존재() throws Exception {
// given
final String url = "/user";
doThrow(new EmailExistsException(ResultCode.DUPLICATE_EMAIL)).when(userService).signUp(any(SignUpRequest.class));
// when
final ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post(url)
.content(gson.toJson(getSignUpRequest()))
.contentType(MediaType.APPLICATION_JSON)
);
// then
resultActions.andExpect(status().isBadRequest());
}
@DisplayName("회원가입_성공")
@Test
void 회원가입_성공() throws Exception {
// given
final String url = "/user";
doReturn(getSignUpRequest().toEntity()).when(userService).signUp(any(SignUpRequest.class));
// when
final ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post(url)
.content(gson.toJson(getSignUpRequest()))
.contentType(MediaType.APPLICATION_JSON)
);
// then
resultActions.andExpect(status().isOk());
}
}
MockMvc
컨트롤러 테스트를 위한 HTTP 호출 객체. 생성할 때 별도의 Exception을 handling 해주는 Controller Advice가 존재하면 set 해준다.
Gson
HTTP 호출 시에 json format으로 변환을 도와주는 객체 (별도의 dependency 추가 필요)
@BeforeEach
테스트 전에 항상 실행할 메소드. 테스트 전에 MockMvc와 Gson 객체를 생생함!
Reference
https://spring.io/guides/gs/testing-web/
https://mangkyu.tistory.com/145
https://mangkyu.tistory.com/182