서비스 테스트 코드의 목표
- 서비스 클래스는 비즈니스 로직을 담당하므로, 테스트의 주요 목적은 비지니스 로직이 의도한 대로 동작하는지 검증하는 것이다. 이를 위해 다음 사항들을 확인해야 한다.
- 정상 동작 : 메서드가 올바른 값을 반환하거나, 예상된 상태 변화를 일으키는지 확인한다.
- 예외 처리 : 유효하지 않은 입력이나 특정 조건(ex: 존재하지 않는 데이터)이 주어졌을 때, 올바른 예외를 던지는지 확인한다.
- 상호작용 확인 : 서비스가 의존하는 다른 컴포넌트(레포지토리, 다른 서비스 등)의 메서드를 올바르게 호출하는지 확인한다.
Mockito를 활용한 의존성 격리
- 서비스는 보통 레포지토리나 다른 서비스를 의존한다. 서비스의 테스트는 단위 테스트이므로, 의존성들이 실제로 데이터베이스나 외부 시스템에 접근하지 않도록 가짜 객체(Mock Object) 를 사용해야 한다.
스프링 환경에서는 Mockito 프레임워크가 가장 널리 사용된다.
@Mock
: 가짜 객체를 만들 필드에 사용한다.
@InjectMocks
: @Mock
객체들을 주입받아 테스트할 대상 클래스에 사용한다.
given()
/ when()
: 가짜 객체의 특정 메서드가 호출될 때 어떤 값을 반환할지 정의한다.
then()
/ verify()
: 특정 메서드가 특정 횟수만큼 호출되었는지 확인한다.
예시 코드
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
void testGetProductWhenProductExists() {
Product product = new Product("laptop", 100);
given(productRepository.findById(1L)).willReturn(Optional.of(product));
ProductDto foundProduct = productService.getProductById(1L);
assertThat(foundProduct.getName()).isEqualTo("laptop");
then(productRepository).should(times(1)).findById(1L);
}
}
명명 규칙
- 테스트 메서드의 이름은 어떤 시나리오를 테스트하는지 명확하게 나타내야 한다.
- test_시나리오결과
- ex: testGetProductWhenProductExists(프로덕트가 존재할 때 프로덕트를 가져오는 테스트)
testCreateProductWithInvalidPrice_throwsException(프로덕트 생성시 가격이 유효하지 않을 때 예외처리를 던지는지 확인하는 테스트)
경계값과 예외 케이스 테스트
- 성공 케이스 외에도 예상치 못한 상황에 대비한 테스트가 중요하다.
- null 값 : 필수 입력값이 null 일 때 적절한 예외를 던지는지 확인
- 빈 칸 : 문자열이 비어있거나 컬렉션이 비어있을 때의 동작 확인
- 음수, 0 : 숫자 타입 입력 시 경계값을 확인
- 존재하지 않는 데이터 :
findById()
와 같이 존재하지 않는 ID로 조회했을 때,
NoSuchElementException
또는 커스텀 예외를 던지는지 확인
사용하는 함수
- assertThat : 주로 값을 검증하는데 사용
- assertThrow : 특정 람다 코드가 실행 될 때 예상하는 예외가 발생하는지 검증하는데 사용
@Test
void findProductById_whenProductNotFound_throwsException() {
given(productRepository.findById(anyLong())).willReturn(Optional.empty());
assertThrows(ProductNotFoundException.class, () -> productService.findProductById(1L));
}
- given, willReturn : 가짜 객체의 메서드가 값을 반환할 때 사용한다.
- doNothing, when : 가짜 객체의 메서드 반환 값이 void일 때 사용한다.
생략해도 되지만 의도를 명확히 하기 위해 사용한다.
doNothing().when(productRepository).deleteById(1L);
- doThrow, when : 메서드가 호출될 때 특정 예외를 발생시키도록 설정한다.
doThrow(new IllegalArgumentException("Invalid product")).when(productRepository).save(any(Product.class));
- verify : 메서드가 특정 횟수만큼 호출되었는지 검증할 때 사용한다.
서비스의 비즈니스 로직이 의존성을 올바르게 활용했는지 확인하는데 필수적으로 사용한다.
verify(productRepository).save(any(Product.class));
- 그 외 verify 옵션
- times(n): n번 호출되었는지 검증
- never(): 한 번도 호출되지 않았는지 검증
- atLeastOnce(): 최소 한 번 이상 호출되었는지 검증
@Test
void createProduct_withValidPrice_savesProduct() {
Product product = new Product("new laptop", 100);
productService.createProduct(product);
then(productRepository).should(times(1)).save(product);
then(productRepository).should(never()).findById(anyLong());
}