본문 바로가기
별밤 일지/개발

[Springboot] Test With Mockito, JUnit

by 별밤 에디터 2024. 3. 5.

오늘은 SpringBoot 프로젝트의 Service, 로직 단 테스트 코드 작성 방법을 작성하려고 한다.

 

▶︎ JUnit

Java에서 독립된 단위테스트를 지원해 주는 프레임워크

Junit에서 지원하는 유용한 Annotation들

  • @Test : 각 테스트가 독립된 UnitTest로 작동할 수 있게 해준다. 각 유닛테스트 단위에 붙이면 된다.
  • @BeforeEach : 각 테스트 실행 전에 실행된다.
    • 모든 테스트에 공통으로 Stubbing을 해야 하는 경우에 사용
    • @Before : 모든 테스트 실행 전에 1번 실행된다.
  • @After : 모든 테스트가 끝난 후 실행된다.
    • 테스트 종료 후에 정리해야 할 데이터가 있는 경우 사용
    • @AfterEach : 각 테스트 종료 후에 한 번 실행된다.

 

▶︎ Mockito

자바 오픈소스 테스트 프레임워크

Mock이라는 가짜 객체를 Stubbing해 사용하여 독립적인 테스트를 진행할 수 있다.

(예를 들어 Service를 테스트하고자 할 때 Repository의 로직이 잘못되었다고 해도 그것과 별개로 Service에서 작성한 로직이 제대로 실행되는지 테스트할 수 있다. 만약 분리하여 테스트할 수 없다면 어디서부터 잘못되었는지 찾기에 더 긴 시간이 걸릴 뿐만 아니라 controller를 테스트하기 위해서 DB연결까지 필요할 수도 있을 것이다. )

 

  • Mock
    Mock은 가짜 객체를 의미한다. 위에서 설명한 것처럼 A class만 독립적으로 테스트 하기 위해서는 A가 사용하는 B,C를 Mock이라는 가짜 객체로 생성해서 사용한다.
    • @Mock
      주입할 객체 (테스트할 클래스에서 사용하는 객체를 의미한다.)
    필요시 여러 개를 주입할 수 있다.
@Mock
    ObservationRepository observationRepository;

 

  • Spy
    • @Spy
      개발자가 Stub한 메소드를 제외하고는 원래 기능을 실행한다. (실제 코드에 작성한 그대로 작동한다.)
@Spy
    ObservationHandler observationHandler;

 

  • @InjectMocks
@InjectMocks
    ObservationService observationService;

    Mock들을 주입 받을 대상을 의미한다. (보통 테스트할 클래스이다.)

    Interface인 경우에 테스트할 클래스의 생성자를 추가해야 한다.

    Test 코드에서는 실제로 test에 사용하는 객체만 잘 들어있으면 된다.

 

  • Stubbing
    Stub한다는 것은 객체의 행동을 미리 지정하는 것을 의미하는데 Mock 객체를 만들었으면 Mock의 행동을 원하는 대로 지정하는 것을 Stubbing이라고 한다.
    mockito의 when().thenReturn 을 사용할 수도 있지만 이번에는 BDDMockito의 given().willReturn을 사용할 것이다. 큰 차이는 없고 BDD가 조금 더 가독성이 좋다는 말이 있다. (취향대로 써도 상관없을 것 같다.)
    given(mock.method()).willReturn(대상)
import static org.mockito.BDDMockito.given;

	@ExtendWith(MockitoExtension.class)// Mockito사용을 위한 어노테이션
public class BeverageServiceTest {
    @InjectMocks
    private BeverageService beverageService;

    @Mock
    private BeverageRepository mockBeverageRepository;

    @Test
    public void whenGetBeverages_thenReturnAllBeverages() {
        //given 특정 상황에
        List<Beverage> beverages = new ArrayList<>();
        Beverage beverage1 = new Beverage("coke",1000,BeverageSize.REGULAR);
        Beverage beverage2 = new Beverage("coke",1000,BeverageSize.SMALL);
        beverages.add(beverage1);
        beverages.add(beverage2);

        given(mockBeverageRepository.findAll()).willReturn(beverages);

        //when 이 행동을 하면
        List<Beverage> results = beverageService.getBeverages();

        //then 이런 결과가 나와야 한다.
        Assertions.assertThat(results).isEqualTo() .hasSameElementsAs(beverages);
    }
}

 

기본적으로 given의 행동을 했을 때 뒤에 붙는 행동을 한다고 생각하면 된다.

  • given(class.method()).willReturn(return값) : 메소드 호출 시 값을 return한다.
  • given().willThrow(Exception) : 메소드 호출 시 Exception을 던진다.

마찬가지로 해당 test에서 사용하는 함수만 stubbing하면 된다.

  • ArgumentMatcher
    함수에 특정 타입의 매개변수를 대입한 상황을 가정한다.
    given()이나 verify()에 대입하는 mock 함수의 인자로 사용한다.
    ex. given(observationRepository.findByIdAndHashTag(anyLong(),any(Hashtag.class)).willReturn(null);
    • anyLong(), anyInt()… : Long, Int 등 각 타입의 매개변수
    • any(Class<T> a) : a 타입의 매개변수
    • eq() : 특정한 값의 매개변수 ex. *eq*(null), eq(”hashtag”)
  • lineant로 공통으로 필요한 행위는 mock객체에 미리 지정해 놓을 수 있다.
@BeforeEach
    public void setUp() throws Exception {
        lenient().when(mockBeverageRepository.getOne(AMERICANO_ID)).thenReturn(new Beverage(AMERICANO_ID, "americano", 1000, BeverageSize.SMALL));
    }

 

  • 검증방법
    위에서 mock과 stubbing을 사용하여 테스트를 독립적으로 돌아가게 했다면 이번엔 기능이 잘 구현되었는지 검증을 하는 방법을 알아보겠다.
    • Assertions.assertThat(result).isEqualTo(observations);
      테스트 결과 값이 의도한 결과로 나왔는지 확인한다.
      Equals 뿐만이 아니라 true,false,범위까지 제공되는 함수로 전부 확인 가능하다.
    • verify(observationRepository, *times*(3)).findByObservationName(eq(*OBSERVATION_NAME)*);
      veryfy를 통해 해당 함수가 몇 번 호출되었는지 확인이 가능하다.
      두 번째 인자로 VerificationMode를 넣어 호출 횟수를 검증할 수 있다.
      • times(int n) : verify의 두 번째 인자로 넣어서 n번 호출되었는지 검증
      • atLeast(int n) : 최소 n번 이상 호출되었는지 검증
      • never() : 수행되지 않았는지 검증 (호출되었으면 실패)
    • 재귀를 사용하거나 재 호출 횟수를 제한한 경우에 확인하는 용도로 사용 가능하다.
  • ArgumentCapture
    함수 내부에 들어가는 인자를 테스트하고 싶을 때 사용한다. 내부적으로 호출하는 함수에 의도한 값이 들어가는지 확인 가능하다.
@Captor
    ArgumentCaptor<Observation> observationCaptor;

@Test
    void createObservationTest() {
        ...
        verify(observationRepository).save(observationCaptor.capture());
        Observation savedResult = observationCaptor.getValue();
        Assertions.assertThat(savedResult.getSaved()).isEqualTo(0);
        Assertions.assertThat(savedResult.getObservationName()).isEqualTo(observationParams.getObservationName());

    }

 

ArgumentCapture.capture() : 인자 값을 수집

ArgumentCapture.getValue() : Capture한 값을 return

캡쳐하고 싶은 type의 ArgumentCapture를 선언하고 verify를 통해 함수 호출 시 인자로 argumentcapture를 사용한다. 이렇게 가져온 함수의 인자 값은 이전과 같이 assertion을 통해 검사할 수 있습니다.

 

  • Exception Test
//1번
Assertions.assertThatThrownBy(()->observationRepository.findByObservationName(OBSERVATION_NAME)).isInstanceOf(RuntimeException.class);
//2번
org.junit.jupiter.api.Assertions.assertThrows()
//3번
@Test(expected = RuntimeException.class)

 

1번은 계속 사용하던 assertion을 사용하는 방법이다. (인자로 Excution형태가 들어가야 한다.)

2번은 다른 라이브러리의 assertThrow를 사용하는 방법이다.

3번은 anotation을 사용하는 방법으로 해당 Exception이 발생해야 Test가 성공한다.

 

▶︎ 그 외의 팁

  • Arrays.asList(item1, item2);
    테스트데이터용 리스트를 만들 때 쉽게 만들 수 있다.
  • Collections.*singletonList*(item1);
    리스트의 객체가 1개일 때 사용하면 메모리 사용량을 줄일 수 있다.
  • @DisplayName
    테스트 코드 메소드 명은 한국어로 작성 가능하다. 이 경우에 영어로도 등록하고 싶다면 해당 어노테이션으로 가능하다.
  • ctrl + shift + t : 같은 자리에 test생성
  • 우클릭 후 More run/debug에 run test with coverage 실행하면 테스트 커버리지를 알 수 있다.
  • 반복되는 값은 맨 위에 상수로 빼면 편하다.
  • 테스트 코드를 작성하기가 어렵다면 테스트하려는 메소드가 복잡하고 여러 기능을 포함한다는 것을 의미할 수 있다. 해당 함수를 리팩토링하는 것을 권한다!

다음에는 Repository단 테스트코드 작성하는 법을 가지고 오겠다.

 


 
별 헤는 밤 놀러가기!

 

별 헤는 밤: 밤하늘, 별자리, 여행정보와 날씨예보까지 - Google Play 앱

오늘부터 별잘알! 오늘밤 별자리 정보, 날씨·광공해·월령까지 고려한 '관측적합도', 주변 관측지 검색으로 누구나 쉽게 밤하늘을 즐겨보세요.

play.google.com