본문 바로가기
Spring Framework/TDD

Junit, Mockito 를 이용한 테스트

by 도쿠니 2022. 6. 11.

테스트시 이점

  • 테스트를 짜면서 코드를 자연스럽게 리뷰하게 됩니다.
  • 테스트 하기 어려운 경우, 코드가 잘못된 것을 빨리 알아챌 수 있습니다.
    • 너무 다양한 일을 하고 역할이 많은 경우 테스트 하기 어렵습니다.
  • 테스트가 잘 되어 있으면 리팩토링이 수월합니다.
    • 코드의 품질이 좋아집니다.
    • 현재 코드의 세부적인 정책들이 모두 테스트로 문서화 됩니다.
  • 장기적으로 볼 때 더 빠르고 안정적인 개발이 가능해집니다.

 

테스트 작성 유의사항

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

 

테스트 도구

JUnit

  • xUnit이라는 유닛테스트 프레임워크를 기반으로 Java용으로 개발된 프레임워크
  • 단위 테스트를 실행하고 결과를 거증해서 전체 결과를 리포트해줍니다.
  • 사용자가 직접 동작시킬 수 있으며 Gradle이나 Maven을 통해 빌드하면서 테스트 가능합니다.
  • spring-boot-starter-test에 기본적으로 Junit5가 포함됩니다.

 

  • 예시
    • @SpringBootTest
      • 스프링 부트에서 통합테스트 시 사용하는 어노테이션
        • 어플리케이션 내부의 모든 빈을 등록해줍니다.
        • webEnvironment.Mock이 기본값으로 설정되어있기에 서블릿 컨테이너가 모킹됩니다.
      • 단위테스트 시에는 사용하지 않습니다.
    • @BeforeEach
      • 각 테스트를 실행하기 전에 수행되는 매번 실행되는 메소드
      • 모든 테스트를 실행하기 전에 딱 한번만 실행하고자 하면 메소드를 static으로 선언하고 @Before 대신 @BeforeAll을 사용합니다.
      • 반대로 동작하는 @After~ 도 있습니다.
    • @Test
      • 테스트를 만드는 모듈역할입니다.
      • @Test가 붙어있는 메소드는 IDE로 직접 실행할 수 있습니다.
    • @DisplayName
      • 테스트 명을 지정할 수 있습니다.
    • assertEquals()
      • 두 객체의 값이 동일한지를 비교하기 위한 메소드입니다.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest 
class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    // 각 테스트 초기화 시에 실행
    @BeforeEach
    void init() {
        accountService.createAccount();
    }

    @Test
    @DisplayName("Test 이름 변경") // 테스트명 변경
    void testGetAccount() {
        Account account = accountService.getAccount(1L);

        assertEquals("4", account.getAccountNumber());
        assertEquals(AccountStatus.IN_USE, account.getAccountStatus());
    }
}
  • Junit만 사용하는 경우 문제점
    • 테스트하고자 하는 클래스가 의존하는 클래스를 모두 만들어야하다보니 테스트를 만들기가 어렵습니다.
    • 모든 클래스가 동작하다보면 어떤 부분이 문제인지 알기가 어렵습니다.

Mockito

  • Mock을 대신 만들어주는 라이브러리.
    • Mock 객체를 직접 구현하지 않아도 자동으로 만들어 주기 때문에 시간을 절약해 줍니다.
    • Mock을 이용하기 때문에 테스트 별로 격리성이 뛰어납니다.
  • 예시
    • Junit 예시를 Mockito방식으로 변경
    • 설명
      • @RunWith(MockitoExtension.class)
        • Mockito에서 제공하는 Mock 객체를 사용하기 위해서 테스트 클래스에 부착합니다.
      • @Mock
        • Mock 객체를 생성합니다.
      • @InjectMocks
        • @Mock이 붙은 Mock 객체를 @InjectMocks가 붙은 객체에 주입시킵니다 (= Autowired)
        • 실무에서는 @InjectMocks(Service) @Mock(Repository) 이런 식으로 사용합니다.
        • 만약 테스트할 객체내의 메소드가 다른 
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
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 testXXX(){
        //given
        given(accountRepository.findById(anyLong()))
                .willReturn(Optional.of(Account.builder()
                        .accountStatus(AccountStatus.UNREGISTERED)
                        .accountNumber("12345")
                        .build()));

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

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

    }
}

 

 

Controller 테스트 방법

Mock

  • 테스트를 위해 만든 모형
  • Mocking = Mock을 만드는 것
  • Mock-up = Mocking 한 객체를 메모리에서 얻어내는 과정
  • 테스트 하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면 의존성을 단절시킬 수 있어 쉽게 테스트 가능합니다.

MockMVC

  • 스프링 MVC 테스트 유틸리티 클래스 입니다.
  • 서블릿 컨테이너를 직접 실행하지 않고 모킹해서 HTTP 요청과 응답을 생성해 줍니다.
    • Postman과 같은 도구를 사용하지 않고도 테스트가 가능해집니다.
    • 서블릿 컨테이너를 모킹한 객체입니다.
    • Controller를 호출해 주는 도구라고 생각하면 됩니다. 
  • MockMvc를 주입받아 사용할 수 있게 해줍니다.

MockMVC 메소드

  • perform()
    • 브라우저에서 서버에 URL 요청을 하듯 컨트롤러를 실행시킬 수 있습니다.
    • RequestBuilder 객체를 인자로 받는데, 이는 MockMvcRequestBuilder의 정적메소드를 이용해서 생성합니다.
    • ResultActions 객체를 반환합니다.

MockMvcRequestBuilders

  • get() , post() , put() , delete() 메소드를 제공합니다.
  • MockHttpServletRequestBuilder 객체를 리턴합니다.

MockHttpServletRequestBuilder

  • 이 객체를 통해 HTTP 요청 관련 정보(파라미터, 헤더, 쿠키 등)를 설정할 수 있습니다.
  • 메시지 체인을 구성하여 복잡한 요청을 설정할 수 있습니다.

ResultActions

  • andExpect()를 통해 응답결과를 검증할 수 있습니다.
    • 이 메소드는 인자로 ResultMatcher를 요구하는데, MockMvcResultMatchers의 정적 메소드를 통해 생성할 수 있습니다.

MockMvcResultMatcher

  • 응답 상태 코드를 검증해줍니다.
isOk() 응답 상태 코드가 정상적인 처리(200)인지 확인
isNotFount() 응답 상태 코드가 404 Not Found인지 확인
isMethodNotAllowed() 응답 상태 코드가 메소드 불일치(405)인지 확인
isInternalServerError() 응답 상태 코드가 예외발생(500)인지 확인
is(int status) 몇 번 응답 상태 코드가 설정되었는지 확인 ex) is(200), is(404)

@SpringBootTest + @AutoConfigureMockMvc

  • 특징
    • 프로젝트 내부에 있는 스프링 빈을 모두 등록하여 테스트에 필요한 의존성을 추가합니다.
    • 실제 운영 환경에서 사용될 클래스들을 통합하여 테스트 합니다.
    • 단위 테스트 같이 기능 검증을 위한 것이 아닌 전체적인 Flow가 제대로 동작하는지 검증하기 위해 사용합니다.
    •  MockMvc를 보다 세밀하게 제어하기 위해서 사용합니다.
  • 장점
    • 운영환경과 가장 유사한 테스트가 가능합니다.
  • 단점
    • 테스트 단위가 크기 때문에 디버깅이 까다롭습니다.
    • 모든 설정과 빈을 로드하기 때문에 시간이 오래걸립니다.
  • @AutoConfigureMockMvc
    • @SpringBootTest에서도 Mock 테스트를 가능하게 해주는 @WebMVcTest와 비슷한 역할 입니다.
    • 다만, Controller 뿐만 아니라 다른 컴포넌트들도 메모리에 올립니다.
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    AccountService accountService;

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

        //when
        //then
        mockMvc.perform(get("/account/1234")) // url로 GET 요청을 전송합니다.
                .andDo(print()) // 응답 값을 화면에 표시해 줍니다.
                .andExpect(jsonPath("$.accountNumber").value("12345")) // 결과를 검증합니다.
                .andExpect(jsonPath("$.accountStatus").value("IN_USE"))
                .andExpect(status().isOk()); // 응답코드를 검증합니다.
    }
}

@WebMvcTest

  • 특징
    • 내가 필요로 하는 MVC 관련 Bean 들만 생성
      • Web Layer만 스캔합니다.
        • Controller , ControllerAdvice, Converter , Filter, HandlerInterceptor 등
        • @Component는 스캔대상에서 제외됩니다.
      • Service 등 Controller에서 의존하는 하위 레이어의 기능은 Mockito가 아닌 스프링에서 지원하는  @MockBean을 통해 모킹해서 원하는 동작을 하도록 합니다.
        • 테스트 하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면 의존성을 단절시킬 수 있어 쉽게 테스트 가능합니다.
      • 단위 테스트가 가능합니다.
  • 장점
    • 필요한 빈만 등록하기 때문에 @SpringBootTest보다 빠릅니다.
    • 통합테스트를 진행하기 어려운 테스트를 개별적으로 진행 가능합니다.
  • 단점
    • Mock을 기반으로 테스트하기 때문에 , 실제 환경에서는 오류가 발생할 수 있습니다.
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


// 테스트하기위한 컨트롤러를 명시하여 Bean 등록하고 MockMvc를 주입해줍니다.
// 컨트롤러 명시를 생략할 경우, 모든 컨트롤러 Bean이 등록됩니다.
@WebMvcTest(AccountController.class)
class AccountControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // 목객체를 빈으로 등록해줍니다. 이 목 빈은 컨트롤러에 DI됩니다.
    @MockBean
    private AccountService accountService;

    @MockBean
    private RedisTestService redisTestService;

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

        //when
        //then
        mockMvc.perform(get("/account/1234")) // url로 GET 요청을 전송합니다.
                .andDo(print()) // 응답 값을 화면에 표시해 줍니다.
                .andExpect(jsonPath("$.accountNumber").value("12345")) // 결과를 검증합니다.
                .andExpect(jsonPath("$.accountStatus").value("IN_USE"))
                .andExpect(status().isOk()); // 응답코드를 검증합니다.
    }
}
  • @SpringBootTest + @AutoConfigureMockMvc의 경우, 테스트 하고자하는 빈만 목으로 만들어주면 다른 빈은 전부 자동으로 등록되기 때문에 테스트하고자 하는 서비스만 코드작성하면 되는데, @WebMvcTest의 경우 컨트롤러 안에 있는 모든 빈을 목으로 만들어주어야 한다.(안하면 예외발생함)

Service 테스트 방법

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());
assertEquals("1234", captor.getValue().getAccountNumber());
  • assertions
    • 다양한 비교 방법들
assertEquals("1234", captor.getValue().getAccountNumber());

assertNotEquals("1234", captor.getValue().getAccountNumber());

assertNull(result);

assertNotNull(result);

assertTrue(resut.getBoolean());

assertFalse(resut.getBoolean());

assertAll(
    () -> assertTrue(resut.getBoolean()),
    () -> assertFalse(resut.getBoolean())
);
  • assertThrows
    • 예외를 던지는 로직을 테스트
AccountException exception =
			assertThrows(AccountException.class, () ->  accountService.getAccount(123L));
            
assertEquals(ACCOUNT_NOT_FOUND, exception.getErrorCode());

'Spring Framework > TDD' 카테고리의 다른 글

단위 테스트 (Unit Test)  (0) 2022.06.21
Mockito  (0) 2022.06.21
JpaAuditing이 Test할 때 적용이 안되는 경우  (0) 2022.06.12

댓글