FIRST규칙
Fast : 테스트는 빠르게 동작하고 자주 가동 해야한다.
Independent : 각각의 테스트는 독립적어이야 하며, 서로에 대한 의존성은 없어야 한다.
Repeatable : 어느 환경에서도 반복이 가능해야 한다.
Self-Validating : 테스트는 성공 또는 실패 값으로 결과를 내어 자체적으로 검증 되어야 한다.
Timely : 테스트는 테스트 하려는 실제 코드를 구현하기 직전에 구현 해야한다.
보통 테스트를 위한 라이브러리로 JUnit과 AssertJ 조합을 사용하여 테스트를 한다.
Given/When/Then 패턴
@Test
@DisplayName("Test해봅시다")
void test() {
// Given
~~~
// When
~~~
// Then
~~~
}
Mockito는 개발자가 동작을 직접 제어할 수 있는 가짜 객체를 지원하는 테스트 프레임워크이다. 일반적으로 Spring으로 웹 애플리케이션을 개발하면 여러 객체들 간의 의존성이 생긴다. 이러한 의존성은 단위 테스트를 작성을 어렵게 하는데 이를 해결하기 위해 가짜 객체를 주입시켜주는 Mockito 라이브러리를 활용할 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 Stub하여 단위 테스트를 진행할 수 있다.
물론 프레임워크 도구가 필요없다면 사용하지 않는 것이 가장 좋다.
Mockito에서 가짜 객체의 의존성 주입을 위해서는 크게 3가지 어노테이션이 사용된다.
@Mock: 가짜 객체를 만들어 반환해주는 어노테이션
@Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
@InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션
UserController에 대한 단위 테스트를 작성하고자 할 때, UserService를 사용하고 있다면 @Mock 어노테이션을 통해 가짜 UserService를 만들고, @InjectMocks를 통해 UserController에 이를 주입시킬 수 있다.
의존성이 있는 객체는 가짜 객체를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시켜야 한다. Mockito에서는 다음과 같은 stub 메소드를 제공한다.
doReturn(): 가짜 객체가 특정한 값을 반환해야 하는 경우
doNothing(): 가짜 객체가 아무 것도 반환하지 않는 경우(void)
doThrow(): 가짜 객체가 예외를 발생시키는 경우
Mockito도 테스팅 프레임워크이기 때문에 JUnit과 결합되기 위해서는 별도의 작업이 필요하다. SpringBoot 2.2.0부터 공식적으로 JUnit5를 지원함에 따라, 이제부터는 @ExtendWith(MockitoExtension.class)를 사용해야 결합이 가능하다.
컨트롤러의 단위 테스트를 하기 위해서 Mockito를 이용하여 다른 계층과 의존관계를 단절 시켜 주어야한다.
컨트롤러가 의존하고 있는 객체는 NotificationServiceImpl, checkPermission 객체다.
컨트롤러를 테스트 하기 위해서는 HTTP 호출이 필요하다. 스프링 부트는 컨트롤러 테스트를 위한 @WebMvcTest 어노테이션을 제공한다.
이를 이용하면 MockMvc 객체가 자동으로 생성될 뿐만 아니라 테스트에 필요한 요소들을 빈으로 등록해 스프링 컨텍스트 환경을 구성 해 준다.
// mock 사용 이유 - controller 테스트는 그저 간단하게 client에게 HTTP Status를 잘 응답하고 있는 지를 테스트하기 위함
// 번거로운 작업을 mocking 함으로써 테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스를 단축
@WebMvcTest(NotificationController.class) // 컨트롤러 테스트를 위한 어노테이션 MockMvc 객체가 자동으로 생성
@MockBean(JpaMetamodelMappingContext.class)
class NotificationControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private NotificationServiceImpl notificationService;
@MockBean
private CheckPermission checkPermission;
@Test
@DisplayName("notification 상태를 true로 변경")
void 알림_상태변경() throws Exception {
CheckNotificationDTO checkNotificationDTO = new CheckNotificationDTO();
checkNotificationDTO.setNotificationId(1);
checkNotificationDTO.setSub("22222");
SuccessReturnDTO successReturnDTO = new SuccessReturnDTO();
successReturnDTO.setSuccess(true);
// 여기에서 any로 줘야지 response의 body부분이 null로 오지 않음
given(notificationService.checkNotification(
any()
)).willReturn(successReturnDTO);
Gson gson = new Gson();
String requestJson = gson.toJson(checkNotificationDTO);
ResultActions resultActions = mockMvc.perform( // controller 테스트에서 요청을 전송함
post("/checknotification") // HTTP 메소드를 결정하고 저 경로로 보냄
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson)
);
resultActions
.andExpect(status().isOk()) // 응답을 검증한다 200이 와야됨
.andExpect(jsonPath("$.success").value(true))
.andDo(result -> { // 요청/응답 전체 메시지를 확인
String contentAsString = result.getResponse().getContentAsString();
System.out.println("Received response: " + contentAsString);
});
}
}
해당 컨트롤러는 Json 데이터 형태로 요청받아 Json 데이터 형태로 반환한다.
객체를 Json 데이터로 변환 해 주기 위해 Gson 라이브러리를 사용하였다.
서비스 계층은 HTTP 호출과 상관 없으며 단순한 로직 검증만 하면 된다.
@ExtendWith(MockitoExtension.class)
class NotificationServiceImplTest {
@Mock
private NotificationRepository notificationRepository;
@InjectMocks //InjectMocks는 생성한 mock객체를 service에 주입
private NotificationServiceImpl notificationService;
@Test
@DisplayName("status true로 변경하는 service 테스트")
void checkNotification() {
CheckNotificationDTO checkNotificationDTO = new CheckNotificationDTO();
checkNotificationDTO.setNotificationId(1);
checkNotificationDTO.setSub("2932421834");
LocalDateTime localDateTime = LocalDateTime.parse("2024-01-06T00:00");
NotificationEntity notificationEntity = new NotificationEntity(
"2932421834", "2932421834", localDateTime, 22, "false", null);
// when에서 동작을 설정하고 thenReturn에서 결과를 지정
// 실제로 데이터베이스에서 값을 가져오는 것이 아닌 단순히 설정된 값을 반환하는 것임 실제 repository 접근 없이 서비스 로직을 테스트함
when(notificationRepository.findById(1)).thenReturn(Optional.ofNullable(notificationEntity));
// 서비스 메서드 호출
SuccessReturnDTO successReturnDTO = notificationService.checkNotification(checkNotificationDTO);
// verify는 when메서드에서 특정 메서드가 몇번 호출되었는지를 확인하는 것이다.
// 상태 변경을 위한 save 메서드 호출 검증
verify(notificationRepository, times(1)).save(any(NotificationEntity.class));
// findById 메서드 호출 횟수 검증
verify(notificationRepository, times(1)).findById(1);
// 반환된 SuccessReturnDTO 검증
assertThat(successReturnDTO.isSuccess()).isEqualTo(true);
// NotificationEntity의 상태 검증
assertThat(notificationEntity.getStatus()).isEqualTo("true");
}
}
JUnit5와 Mockito를 연동하기 위해서는 @ExtendWith(MockitoExtension.class)를 사용해야한다.
의존성 주입을 위해 NotificationRepository에 @Mock을 이용하여 Mock 객체를 생성해 주고.
생성된 Mock 객체를 NotificationServiceImpl에 @InjectMocks 어노테이션을 이용하여 주입해 준다.