본문 바로가기

Develop/Spring

Spring Test (JUnit, Mockito, Spring Boot)

728x90

Spring Test (JUnit,  Mockito, Spring Boot)

옛날에 테스트라 함은...

노동 집약적인 테스트 진행

  • 자동화된 테스트가 어려운 코드들이 많았음(SQL 중심의 코드 트리거, 패키지, 프로시저 등)
  • 1회성 개발이 많았기 때문에(개발 후 철수), 코드의 품질보다는 기능적인 완성도만 체크를 했었습니다.
  • FE/BE가 나눠져 있지 않고 서버에서 로직과 화면까지 모두 만들어서 보내는 구조가 많았기 때문에
    테스트 자동화라고 해도 실제 화면에서 직접 마우스/키보드가 움직이는 것처럼 하는 테스트가 많았습니다.

현재의 테스트

  • 자동화된 테스트
  • 빠른 테스트
  • 여러가지 테스트와 방법론 등 테스트의 발전
  • 테스트가 중요하다는 것은 이미 너무 많은 사람들이 알고 있습니다.

테스트를 잘하면 좋은 점

  • 테스트를 짜면서 나의 코드를 자연스럽게 셀프 코드 리뷰하게 됩니다.
  • 테스트 하기가 어려운 경우 코드가 잘못된 것을 빨리 알아낼 수 있습니다.
    예) 너무 다양한 일을 하고 역할이 많은 경우
  • 테스트가 잘 되어 있으면 마음껏 리팩토링 할 수 있습니다.
    1. 코드의 품질이 좋아집니다.
    2. 현재 코드의 아주 세부적인 정책들이 모두 테스트로 문서화가 됩니다.
    3. 다음에 기능을 추가하거나 변경할 때도 더 편하게 기존 기능의 영향도를 알 수 있고 안심하며 개발이 가능합니다.
결론 > 장기적으로 볼 때 더 빠르고 안정적인 개발이 가능하게 됩니다.

테스트를 잘 하려면

  • 클래스나 메서드가 SRP를 지키고 너무 크지 않아야 합니다.
  • 유닛 테스트의 경우 적절한 Mocking으로 격리성을 확보합니다.
  • 테스트 커버리지를 높여서 테스트가 안되는 부분이 없도록 합니다.

JUnit

JUnit은 xUnit이라는 유닛테스트 프레임워크의 일환으로 Java용으로 개발된 프레임워크입니다.

 

https://junit.org/junit5/

 

JUnit 5

The JUnit team uses GitHub for version control, project management, and CI.

junit.org

  • JUnit은 단위 테스트를 실행하고 결과를 검증해서 전체 결과를 리포트 해주는 프레임워크 입니다.
  • 사용자가 직접 동작시킬 수도 있으며 Gradle이나 Maven 등을 통해 빌드하면서 테스트도 가능합니다.
  • spring-boot-starter-test에 기본적으로 JUnit5가 포함되었습니다.

Mokito

이름과 비슷하게 Mock을 만들어 주는 라이브러리 입니다.

해결하고자 하는 점?

  • 테스트하고자 하는 클래스가 의존하는 클래스를 모두 만들려다보니 테스트 만들기가 어렵습니다.
  • 모든 클래스가 동작하다보면 어떤 부분이 문제인지 알기가 어렵습니다.

가짜(Mock)를 만들어서 내가 원하는 방식으로 동작하게 하자! Mockito 라이브러리 활용

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
class AccountServiceTest {
    @Mock
    private AccountRepository accountRepository;

    @InjectMocks
    private AccountService accountService;

    @Test
    @DisplayName("계좌 조회 성공")
    void getAccountTest() {
        // given
        given(accountRepository.findById(anyLong()))
                .willReturn(Optional.of(Account.builder()
                        .accountStatus(AccountStatus.UNREGISTERED)
                        .accountNumber("65789")
                        .build())
                );

        // when
        Account account = accountService.getAccount(4555L);

        // then
        assertEquals("65789", account.getAccountNumber());
        assertEquals(AccountStatus.UNREGISTERED, account.getAccountStatus());
    }
}

Controller 테스트 방법

컨트롤러를 테스트하는 2가지 대표적인 방법

1. @SpringBootTest + @AutoConfigureMockMvc

  • 전체 Bean을 모두 생성한 후
  • mockMvc를 통해 http 요청과 검증을 진행

2. @WebMvcTest

내가 필요로 하는 MVC관련 Bean들만 생성

  • Controller, ControllerAdvice, Converter, Filter, HandlerInterceptor 등
  • Service 등 Controller에서 의존하는 하위 레이어의 기능은 @MockBean을 통해 모킹해서 원하는 동작을 하도록 합니다.
    (Mockito와 유사한 방식)
@WebMvcTest(AccountController.class)
class AccountControllerTest {
    @MockBean
    private AccountService accountService;

    @MockBean
    private RedisTestService redisTestService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void successGetAccount() throws Exception {
        // given
        given(accountService.getAccount(anyLong()))
                .willReturn(Account.builder()
                        .accountNumber("3456")
                        .accountStatus(AccountStatus.IN_USE)
                        .build());

        // when


        // then
        mockMvc.perform(MockMvcRequestBuilders.get("/account/876"))
                .andDo(print())
                .andExpect(jsonPath("$.accountNumber").value("3456"))
                .andExpect(jsonPath("$.accountStatus").value("IN_USE"))
                .andExpect(status().isOk());
    }
}

Service 테스트 방법

verify

의존하고 있는 Mock이 해당되는 동작을 수행했는지 확인하는 검증입니다.

verify(accountRepository, times(1)).save(any<Account>());
verify(accountRepository, times(0)).findById(anyLong());

ArgumentCaptor

의존하고 있는 Mock에 전달된 데이터가 내가 의도하는 데이터가 맞는지 검증

ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class); // 빈 박스 같은 것
verify(accountRepository, times(1)).save(captor.capture());
assetEquals("1234", captor.getValue().getAccountNumber());

Assertions

다양한 단언(assertion) 방법들

assertEquals("1234", captor.getValue().getAccountNumber());
assertNotEquals("1234", captor.getValue().getAccountNumber());
assertNull(result);
assertNotNull(result);
assertTrue(result.getBoolean());
assertFalse(result.getBoolean());
assertAll(
    () -> assertTrue(result.getBoolean()),
    () -> assertFalse(result.getBoolean())
);

*  assertAll 위의 코드들 처럼 나열하면 중간에 검사 실패가 발생할 시 그 밑을 수행하지 않지만
assertAll을 사용할 시 전체를 항상 검사하니 다양한 결과를 한 눈에 볼 수 있습니다.

AssertThrows

예외를 던지는 로직을 테스트하는 방법

AccountException exception =
                               assertThrows(AccountException.class, () ->
                                   accountService.getAccount(123L));
assertEquals(ACCOUNT_NOT_FOUND, exception.getErrorCode());

'Develop > Spring' 카테고리의 다른 글

JpaRepository 대신 Repository 를 사용해야 하는 이유  (0) 2023.02.19
Spring Boot ServletInitializer  (0) 2022.12.15
Spring Exception  (0) 2022.11.19
Spring Filter, Interceptor  (0) 2022.11.17
Spring MVC  (0) 2022.11.17