오늘은 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
@Mock
ObservationRepository observationRepository;
- Spy
- @Spy
개발자가 Stub한 메소드를 제외하고는 원래 기능을 실행한다. (실제 코드에 작성한 그대로 작동한다.)
- @Spy
@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() : 수행되지 않았는지 검증 (호출되었으면 실패)
- 재귀를 사용하거나 재 호출 횟수를 제한한 경우에 확인하는 용도로 사용 가능하다.
- Assertions.assertThat(result).isEqualTo(observations);
- 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단 테스트코드 작성하는 법을 가지고 오겠다.
'별밤 일지 > 개발' 카테고리의 다른 글
[Flutter] Retrofit 적용 (0) | 2024.04.16 |
---|---|
[Java] 리플렉션, ObjectMapper (2) | 2024.03.12 |
[Android.hardware.Sensor] 안드로이드 Sensor를 활용하여 방위각, 고도 구하기 (0) | 2024.02.20 |
[Spring] Logback 을 활용한 로깅하기 (0) | 2024.01.30 |
[QueryDSL] queryDSL 도입기 (0) | 2024.01.28 |