[25/09/03] 테스트 코드 - 서비스

김기수·2025년 9월 3일
0

서비스 테스트 코드의 목표

  • 서비스 클래스는 비즈니스 로직을 담당하므로, 테스트의 주요 목적은 비지니스 로직이 의도한 대로 동작하는지 검증하는 것이다. 이를 위해 다음 사항들을 확인해야 한다.
    • 정상 동작 : 메서드가 올바른 값을 반환하거나, 예상된 상태 변화를 일으키는지 확인한다.
    • 예외 처리 : 유효하지 않은 입력이나 특정 조건(ex: 존재하지 않는 데이터)이 주어졌을 때, 올바른 예외를 던지는지 확인한다.
    • 상호작용 확인 : 서비스가 의존하는 다른 컴포넌트(레포지토리, 다른 서비스 등)의 메서드를 올바르게 호출하는지 확인한다.

Mockito를 활용한 의존성 격리

  • 서비스는 보통 레포지토리나 다른 서비스를 의존한다. 서비스의 테스트는 단위 테스트이므로, 의존성들이 실제로 데이터베이스나 외부 시스템에 접근하지 않도록 가짜 객체(Mock Object) 를 사용해야 한다.
    스프링 환경에서는 Mockito 프레임워크가 가장 널리 사용된다.
  • @Mock : 가짜 객체를 만들 필드에 사용한다.
  • @InjectMocks : @Mock 객체들을 주입받아 테스트할 대상 클래스에 사용한다.
  • given() / when() : 가짜 객체의 특정 메서드가 호출될 때 어떤 값을 반환할지 정의한다.
  • then() / verify() : 특정 메서드가 특정 횟수만큼 호출되었는지 확인한다.

예시 코드

// Extendwith: 단위 테스트에 공통적으로 사용할 확장 기능을 선언, 인자로 확장할 Extention을 명시하면 된다.
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
	// Mock: 가짜 객체 생성, 실제로 메서드는 갖고 있지만 내부 구현은 없음
    @Mock
    private ProductRepository productRepository;
    
	// InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 객체
    // @InjectMocks 객체에서 사용할 객체를 @Mock으로 만들어 쓰면 된다.
    // 만약 Service를 테스트하는 클래스를 생성했다면, 
    // Service 객체를 @InjectMocks 어노테이션을 사용해 생성하고, 
    // Service단에서 사용할 Repository와 같은 객체들은 @Mock 어노테이션을 사용해 생성하면 된다.
    @InjectMocks
    private ProductService productService;

    @Test
    void testGetProductWhenProductExists() {
        // given: 테스트에 필요한 초기 조건(데이터)설정
        Product product = new Product("laptop", 100);
        // ProductRepository가 특정 ID로 호출될 때 Product 엔티티를 반환하도록 설정
        given(productRepository.findById(1L)).willReturn(Optional.of(product));

        // when: 테스트 대상 메서드 호출
        ProductDto foundProduct = productService.getProductById(1L);

        // then: 결과 검증 및 메서드 호출 확인
        assertThat(foundProduct.getName()).isEqualTo("laptop");
        then(productRepository).should(times(1)).findById(1L); // findById가 1번 호출되었는지 검증
    }
}

명명 규칙

  • 테스트 메서드의 이름은 어떤 시나리오를 테스트하는지 명확하게 나타내야 한다.
  • test_시나리오결과
  • ex: testGetProductWhenProductExists(프로덕트가 존재할 때 프로덕트를 가져오는 테스트)
    testCreateProductWithInvalidPrice_throwsException(프로덕트 생성시 가격이 유효하지 않을 때 예외처리를 던지는지 확인하는 테스트)

경계값과 예외 케이스 테스트

  • 성공 케이스 외에도 예상치 못한 상황에 대비한 테스트가 중요하다.
    • null 값 : 필수 입력값이 null 일 때 적절한 예외를 던지는지 확인
    • 빈 칸 : 문자열이 비어있거나 컬렉션이 비어있을 때의 동작 확인
    • 음수, 0 : 숫자 타입 입력 시 경계값을 확인
    • 존재하지 않는 데이터 : findById()와 같이 존재하지 않는 ID로 조회했을 때,
      NoSuchElementException 또는 커스텀 예외를 던지는지 확인

사용하는 함수

  • assertThat : 주로 값을 검증하는데 사용
  • assertThrow : 특정 람다 코드가 실행 될 때 예상하는 예외가 발생하는지 검증하는데 사용
@Test
void findProductById_whenProductNotFound_throwsException() {
    // given
    given(productRepository.findById(anyLong())).willReturn(Optional.empty());

    // when & then
    // findById(1L) 메서드 실행 시 ProductNotFoundException이 발생하는지 검증
    assertThrows(ProductNotFoundException.class, () -> productService.findProductById(1L));
}
  • given, willReturn : 가짜 객체의 메서드가 값을 반환할 때 사용한다.
  • doNothing, when : 가짜 객체의 메서드 반환 값이 void일 때 사용한다.
    생략해도 되지만 의도를 명확히 하기 위해 사용한다.
// `productRepository.deleteById(1L)`이 호출될 때 아무것도 하지 않음
doNothing().when(productRepository).deleteById(1L);
  • doThrow, when : 메서드가 호출될 때 특정 예외를 발생시키도록 설정한다.
// `productRepository.save()`가 호출될 때 `IllegalArgumentException`을 던짐
doThrow(new IllegalArgumentException("Invalid product")).when(productRepository).save(any(Product.class));
  • verify : 메서드가 특정 횟수만큼 호출되었는지 검증할 때 사용한다.
    서비스의 비즈니스 로직이 의존성을 올바르게 활용했는지 확인하는데 필수적으로 사용한다.
// save 메서드가 1번 호출되었는지 검증
verify(productRepository).save(any(Product.class));
  • 그 외 verify 옵션
    • times(n): n번 호출되었는지 검증
    • never(): 한 번도 호출되지 않았는지 검증
    • atLeastOnce(): 최소 한 번 이상 호출되었는지 검증
@Test
void createProduct_withValidPrice_savesProduct() {
    // given
    Product product = new Product("new laptop", 100);

    // when
    productService.createProduct(product);

    // then
    // save 메서드가 호출되었는지 검증
    then(productRepository).should(times(1)).save(product);

    // findById는 호출되지 않았는지 검증
    then(productRepository).should(never()).findById(anyLong());
}
profile
백엔드 개발자

0개의 댓글