SpringBoot - 단위 테스트

Tae Yun Choi·2023년 5월 16일
0

개발새발 SpringBoot

목록 보기
2/2
post-thumbnail

💡 기본적인 테스트 개념에 대해 알아보고 Spring에서 사용하는 테스트 방법들에 대해 정리해보고자 한다.

테스트란?

  • 구현한 프로그램이 올바르게 작동하는지 검사하는 과정
  • 테스트 코드를 작성해두면, 추후에 리팩토링 할 때에 자신감 있게 리팩토링이 가능하다 → 기능이 제대로 동작하는지 바로 확인할 수 있기 때문
  • 기능을 구현하고 테스트 코드를 작성하는 습관을 들이도록 노력해보자

테스트의 종류

단위 테스트

  • 단위 테스트는 프로그램에서 테스트 가능한 가장 작은 부분을 실행하여 예상대로 동작하는지 확인하는 과정이다.
  • 단위 테스트에서 테스트 단위 크기는 절대적으로 정해져 있지는 않지만, 일반적으로 클래스 혹은 메소드 수준으로 정해진다.
    • Solitary vs Sociable
      • Solitary test: TestDouble(Dummy, Fake Stubs, Spies, Mocks)를 사용하여 의존성이 있는 클래스 등을 완벽히 분리시켜 테스트
      • Sociable Test: 의존성이 있는 다른 코드들과 함께 테스트
  • 단위가 작을수록 검증하기 쉬워지며, 동작 표현이 쉬워진다.

통합 테스트

  • 통합 테스트는 주로 개발자가 영향을 끼칠 수 없는 부분까지 묶어서 검증할 떄 주로 사용한다.
  • 전체 코드와 다양한 환경이 함께 제대로 동작하는지 확인하기에 단위 테스트에서 발견하기 어려운 버그 등을 찾을 수도 있다.
  • 그러나 많은 코드를 동시에 테스트하기 때문에 단위테스트보다 신뢰성이 떨어질 수 있고, 에러가 발생한 지점을 찾기 어려울 수도 있다.

그 외에도 인수 테스트(시나리오 테스트), E2E테스트 (EndToEnd 테스트), 스트레스 테스트(부하 테스트) 등이 있지만, 이에 대해선 아직 접해본 적이 없기에 다음에 다뤄볼 기회가 생기면 그때 정리를 해보려 한다.

오늘은 스프링에서 사용되는 단위, 통합 테스트에 대해 간략히 알아보자

스프링에서 단위, 통합 테스트를 어떻게 활용하는지 알아보기에 앞서 테스트에 관해 이야기 할 때 항상 나오는 Mock과 Stub에 대해 간략히 알아보자


Mock이란?

  • Mock은 간단히 말하면 가짜 객체를 의미한다.
  • 실제와 동일한 기능을 하지는 않지만, 대충 이러한 기능이 동작할 것이라고 알려주는 용도
  • 테스트에서는 호출 시 동작이 잘 되었는 지를 확인하는 용도로 쓰임
  • 즉, 테스트에서 Mock을 통하여 동작 수행 여부에 포커싱하여 테스트한다.
  • 실제 객체의 구현이 끝나지 않았거나, 테스트 하려는 부분에 다른 객체의 의존성이 강하게 결합되어 있어 테스트를 구현하기 어려울 경우 Mock을 만들어 사용한다.
  • Spring에서는 주로 Mockito라는 테스트 프레임워크를 사용하여 Mock을 생성하고, 사용한다

Stub이란?

  • stub이란 전체 중 일부라는 뜻.
  • 모든 기능 대신 일부 기능에 집중하여 임의로 구현
  • 일부 기능은 테스트를 하고자 하는 기능

Mockito란?

  • 단위 테스트를 위한 자바 Mocking 프레임워크
  • 가장 보편적인 Mocking 프레임 워키으며, 테스트 더블 중 Mock 객체를 필요로 할 때 주로 사용한다

Mockito + JUnit

  • Mockito도 JUnit과는 별도의 테스트 프레임워크 이기에, 두개를 결합하려면 별도의 작업이 필요하다.
  • JUnit4에서는 @RunWith(MockitoJUnitRunner.class)를 붙여서 결합을 했지만,
  • SpringBoot 2.2.0부터 JUnit5를 지원하게 되었고, 이제는 @ExtendWith(MockitoExtension.class)를 사용하여 결합이 가능하다

Mockito를 활용한 Mock 객체 의존성주입

@ExtendWith(MockitoExtension.class)
public class MockTests {
    @Mock
    private OrderRepository orderRepository;
    @InjectMocks
    private OrderService orderService;   
}

/**************/

@SpringBootTest
public class MockTests {
    @MockBean
    private OrderRepository orderRepository;
    @Autowired
    private OrderService orderService;   
}

@Mock

  • 가짜 객체를 만들어 반환해주는 어노테이션

@Spy

  • Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
    • stub이란 다른 객체 등을 일시적으로 대체하는 구성 요소
    • 즉 spy는 대체되지 않은 메소드들은 원본 메소드를 그대로 사용한다는의미

@InjectMocks

  • @Mock or @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션
  • InjectMocks는 스프링 컨텍스트에서 빈을 찾지 않으므로 MockBean과 함께 사용하지 못한다.

@MockBean

  • Spring의 ApplicationContext가 Mock 객체를 빈으로 등록하며, 동일한 타입의 동일한 객체가 이미 빈에 등록되어 있을 경우, 해당 빈은 선언한 객체로 대체
  • Autowired로 등록된 mock객체와 함께 의존성 처리를 해준다

Mockito vs BDDMockito

  • BDD란, Behavior-Driven Development의 약자
  • 행위 주도 개발을 의미
  • 테스트 대상의 상태의 변화를 테스트하는 것
  • 시나리오를 기반으로 테스트하는 패턴
  • 기본 패턴은 Given, When, Then 구조
    • 어떠한 상태에서 출발하며 (Given)
    • 어떤 상태 변화를 가했을 때 (When)
    • 기대하는 상태로 완료되어야 함 (Then)
public class PhoneBookService {
    private PhoneBookRepository phoneBookRepository;

    public void register(String name, String phone) {
        if(!name.isEmpty() && !phone.isEmpty()
          && !phoneBookRepository.contains(name)) {
            phoneBookRepository.insert(name, phone);
        }
    }

    public String search(String name) {
        if(!name.isEmpty() && phoneBookRepository.contains(name)) {
            return phoneBookRepository.getPhoneNumberByContactName(name);
        }
        return null;
    }
}

/***********************/

// Tranditional Mockito

// given
when(phoneBookRepository.contains(momContactName))
  .thenReturn(false);
// doReturn(false).when(phoneRepository).contains(momContactName)
 
// when
phoneBookService.register(momContactName, momPhoneNumber);
 
// then
verify(phoneBookRepository)
  .insert(momContactName, momPhoneNumber);

/*

테스트 대상의 repository가 contains 메서드를 실행하면 
false만 반환하는 상태에서 출발하여(Given)
register 메서드를 실행했을 때
insert 메서드가 한번 실행되어야 함

Tranditional Mockito는 given 부분에서 when 메서드를 사용하여
문맥상 혼란이 있을 수 있음
이를 해결하기 위하여 BDD Mockito를 통해 아래와 같이 수정
코드의 로직은 동일 

*/

/***********************/

// BDDMockito
// given
given(phoneBookRepository.contains(momContactName))
  .willReturn(false);

// when
phoneBookService.register(momContactName, momPhoneNumber);

// then
then(phoneBookRepository)
  .should()
  .insert(momContactName, momPhoneNumber);

/***********************/

// Returning a Fixed Value
given(phoneBookRepository.contains(momContactName))
  .willReturn(false);
 
phoneBookService.register(xContactName, "");
 
then(phoneBookRepository)
  .should(never())
  .insert(momContactName, momPhoneNumber); 

/***********************/

// Returning a Dynamic Value

given(phoneBookRepository.contains(momContactName))
  .willReturn(true);
given(phoneBookRepository.getPhoneNumberByContactName(momContactName))
  .will((InvocationOnMock invocation) ->
    invocation.getArgument(0).equals(momContactName) 
      ? momPhoneNumber 
      : null);
phoneBookService.search(momContactName);
then(phoneBookRepository)
  .should()
  .getPhoneNumberByContactName(momContactName);

/***********************/

// Throwing an Exception
given(phoneBookRepository.contains(xContactName))
  .willReturn(false);
willThrow(new RuntimeException())
  .given(phoneBookRepository)
  .insert(any(String.class), eq(tooLongPhoneNumber));

try {
    phoneBookService.register(xContactName, tooLongPhoneNumber);
    fail("Should throw exception");
} catch (RuntimeException ex) { }

then(phoneBookRepository)
  .should(never())
  .insert(momContactName, tooLongPhoneNumber);
  • BDDMockito는 Mockito를 상속받은 클래스로, 동작 및 사용하는 방법이 거의 비슷하다.

@SpringBootTest

  • 해당 어노테이션은 프로젝트 전체의 컨텍스트를 로드 및 빈에 주입하며, 통합 테스트를 할 때 많이 사용한다.
  • 그러나 모든 컨텍스트를 로드하고, 빈에 주입하기 때문에 속도가 느리다는 단점이 있다.
  • 따라서 통합 테스트를 진행하는 것이 아닌, 단위 테스트를 진행할 때에는 필요한 빈만 등록하여 테스트를 진행하기 위해 슬라이스 테스트 어노테이션을 사용하는 것이 더욱 효율적이다

@AutoConfigureMockMvc

  • @WebMvcTest가 아닌 @SpringBootTest에서도 Mock 테스트를 가능하게 해준다.

@WebMvcTest

  • MVC 부분 슬라이스 테스트로, 서블릿 컨테이너를 모킹해준다.
  • 웹 환경에서 컨트롤러를 테스트하려면, 서블릿 컨테이너가 구동되고, DispatcherServlet 객체가 메모리에 올라가야 하지만, 서블릿 컨테이너를 모킹하면 실제 서블릿 컨테이너가 아닌 테스트용 컨테이너를 사용하기 때문에 간단하게 테스트가 가능하다.
  • 주로 컨트롤러 하나만 테스트하고 싶을 때 사용한다.
  • 테스트를 원하는 컨트롤러 클래스를 어노테이션의 인자로 넣어준다.
    • @WebMvcText(APIController.class)

서블릿 간단 정리

  • DispatcherServlet란 HTTP프로토콜로 들어오는 모든 요청을 먼저 받아 라우팅해주는 프론트 컨트롤러
  • 서블릿 컨테이너란 서블릿들의 생성, 실행, 파괴를 담당하는 서블릿들을 관리하는 공간, client의 request를 받아 response할 수 있게 웹 서버 및 소켓을 만들어 통신함. 대표적인 서비스로 Tomcat이 있음
  • 서블릿 동작 방식

  1. 사용자(클라이언트)가 URL을 입력하면 HTTP Request가 Servlet Container로 전송합니다.
  2. 요청을 전송받은 Servlet Container는 HttpServletRequest, HttpServletResponse 객체를 생성합니다.
  3. web.xml을 기반으로 사용자가 요청한 URL이 어느 서블릿에 대한 요청인지 찾습니다.
  4. 해당 서블릿에서 service메소드를 호출한 후 클리아언트의 GET, POST여부에 따라 doGet() 또는 doPost()를 호출합니다.
  5. doGet() or doPost() 메소드는 동적 페이지를 생성한 후 HttpServletResponse객체에 응답을 보냅니다.
  6. 응답이 끝나면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킵니다.

MockMvc mockMvc

  • 웹 API를 테스트할 때 사용한다
  • 스프링 MVC 테스트의 시작점이다.
  • 해당 클래스를 통해 HTTP Method에 대한 API 테스트를 할 수 있다.

mockMvc.perform(get(”/hello”))

  • MockMvc를 통해 “/hello” 주소로 GET요청을 보낸다.

.andExpect(status().isOK())

  • perform의 실행 결과를 검증한다.

출처

profile
hello dev!!

0개의 댓글