[Spring Boot] 단위테스트 (Feat. Mockito)

diense_kk·2024년 1월 7일
0

SpringBoot

목록 보기
9/10
post-thumbnail

단위 테스트와 통합 테스트


단위 테스트

  • 단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트다.
  • 하나의 모듈이란 각 계층에서의 하나의 기능 또는 메소드로 이해할 수 있다.
  • 하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것이다.

단위 테스트의 필요성

  • 통합 테스트는 실제 여러 컴포넌트들 간의 상호작용을 테스트 하기 때문에 모든 컴포넌트들이 구동된 상태에서 테스트를 하게 되므로, 캐시나 데이터베이스 등 다른 컴포넌트들과 실제 연결을 해야하고 어플리케이션을 구성하는 컴포넌트들이 많아 질수록 테스트를 위한 시간이 커진다.
  • 하지만, 단위 테스트는 테스트하고자 하는 부분만 독립적으로 테스트를 하기 때문에 해당 단위를 유지 보수 또는 리팩토링 하더라도 빠르게 문제 여부를 확인 할 수 있다.

단위 테스트의 한계

  • 단위 테스트는 해당 기능에 대한 독립적인 테스트기 때문에 다른 객체와 데이터를 주고 받는 경우에 문제가 발생한다.
  • 그래서 이 문제를 해결하기 위해 테스트하고자 하는 기능과 연관된 모듈에서 가짜 데이터, 정해진 반환값이 필요하다.
  • 즉 단위 테스트에서는 테스트 하고자 하는 기능과 연관된 다른 모듈은 연결이 단절 되어야 비로소 독립적인 단위 테스트가 가능해 진다.

단위 테스트의 특징

  • 좋은 테스트 코드란, 계속해서 변하는 요구사항에 맞춰 변경된 코드는 버그의 가능성을 항상 내포하고 있으며, 이를 테스트 코드로 검증함으로써 해결할 수 있어야 한다.
  • 실제 코드가 변경되면 테스트 코드도 변경이 필요할 수 있으며, 테스트 코드 역시 가독성 있게 작성하여 일관된 규칙과 일관된 목적으로 테스트 코드를 작성 해야한다.

FIRST규칙

Fast : 테스트는 빠르게 동작하고 자주 가동 해야한다.
Independent : 각각의 테스트는 독립적어이야 하며, 서로에 대한 의존성은 없어야 한다.
Repeatable : 어느 환경에서도 반복이 가능해야 한다.
Self-Validating : 테스트는 성공 또는 실패 값으로 결과를 내어 자체적으로 검증 되어야 한다.
Timely : 테스트는 테스트 하려는 실제 코드를 구현하기 직전에 구현 해야한다.


통합 테스트

  • 모듈을 통합하는 과정에서 모듈 간 호환성을 확인하기 위한 테스트다.
  • 다른 객체들과 데이터를 주고받으며 복잡한 기능이 수행 될때, 연관된 객체들과 올바르게 동작하는지 검증하고자 하는 테스트다.
  • 독립적인 기능보다 전체적인 연관 기능과 웹 페이지로 부터 API를 호출하여 올바르게 동작하는지 확인한다.

테스트 코드 작성시 준수 사항

보통 테스트를 위한 라이브러리로 JUnit과 AssertJ 조합을 사용하여 테스트를 한다.

Given/When/Then 패턴

  • Given : 어떠한 데이터가 주어질 때.
  • When : 어떠한 기능을 실행하면.
  • Then : 어떠한 결과를 기대한다.
@Test
  @DisplayName("Test해봅시다")
  void test() {
      // Given
		~~~
      // When
		~~~
      // Then
      	~~~
  }

Mockito

Mockito는 개발자가 동작을 직접 제어할 수 있는 가짜 객체를 지원하는 테스트 프레임워크이다. 일반적으로 Spring으로 웹 애플리케이션을 개발하면 여러 객체들 간의 의존성이 생긴다. 이러한 의존성은 단위 테스트를 작성을 어렵게 하는데 이를 해결하기 위해 가짜 객체를 주입시켜주는 Mockito 라이브러리를 활용할 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 Stub하여 단위 테스트를 진행할 수 있다.
물론 프레임워크 도구가 필요없다면 사용하지 않는 것이 가장 좋다.

Mockito 사용법

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 어노테이션을 이용하여 주입해 준다.

0개의 댓글