Service계층 단위 테스트

양성준·2025년 3월 21일

스프링

목록 보기
18/49

Spring Boot 단위 테스트 개요

(BasicUserServiceTest 기준)

1. 테스트 관련 어노테이션

@ExtendWith(MockitoExtension.class)

  • Mockito를 이용한 단위 테스트를 실행할 때 사용
  • Spring Context를 로드하지 않음 → 가벼운 단위 테스트 작성 가능
  • Mockito를 초기화하여 @Mock, @InjectMocks 등의 어노테이션을 사용할 수 있도록 함
    @Mock
  • 가짜(Mock) 객체를 생성하여 실제 객체를 대신하도록 만듦
  • userRepository, userStatusRepository, binaryContentRepository를 실제 구현체 없이 테스트 가능하도록 설정
  • Mockito.when()을 사용하여 동작을 정의할 수 있음

@InjectMocks

  • @Mock으로 주입된 객체를 사용하여 테스트할 클래스의 인스턴스를 자동 생성
  • 즉, 의존성이 자동으로 주입된 상태의 BasicUserService를 만듦

@BeforeEach

  • 각 테스트 실행 전에 호출되는 초기화 메서드
  • 테스트에 사용할 더미 객체(mockUser, mockUserStatus)를 생성
  • 테스트마다 새로운 객체를 생성하여 테스트 격리(Isolation) 보장

@Test

  • JUnit5의 테스트 메서드를 정의
  • 이 어노테이션이 붙은 메서드는 독립적으로 실행됨

2. 필요한 의존성 추가

  • Spring Boot Starter Test에는 JUnit5, AssertJ, Mockito가 포함되어 있어 별도의 추가가 필요하지 않음.
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  • 하지만, 개별적으로 추가할 수도 있다.
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testImplementation 'org.mockito:mockito-core'
    testImplementation 'org.assertj:assertj-core'
}

3. static import 활용

  • assertEquals(), assertThatThrownBy(), verify() 등의 메서드를 가독성 높게 사용할 수 있도록 static import 사용을 권장.
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

4. 테스트 대상 선정 원칙

  • 테스트를 작성할 때, "무엇을 테스트해야 하고, 무엇을 생략해야 하는지"가 중요하다.

반드시 테스트해야 하는 대상

  • 비즈니스 로직이 정상적으로 성공하는 경우
  • 비즈니스 로직에서 예외를 던지는 경우 → 예외를 제대로 던지는지 반드시 테스트 필요
    • findById() → 찾지 못하면 Optional.empty() 예외 발생
    • existsByUsername(), existsByEmail() → 중복 체크 예외 발생
    • update() → 유저가 없으면 예외 발생
    • delete() → 유저가 없으면 예외 발생
  • 이런 경우 반드시 성공 케이스 & 예외 케이스 테스트 작성

테스트 불필요한 대상

  • 단순히 Repository 기능을 호출하는 경우 → 굳이 테스트할 필요 없음
    • Repository 기능은 Repository 단위 테스트에서 검증!
  • 다른 Service를 주입받았을 경우 -> 지금이 아니라, 다른 Service를 Test할 때 검증하면 됨!
  • 반환값을 로직에서 사용하지 않는다면, 굳이 given / willReturn으로 mock 해줄 필요가 없다.
  • 단순 호출 여부만 verify()로 확인하면 충분함

5. 테스트 케이스 선정 기준

  • 테스트할 내용
  • 정상적인 경우 → 성공 테스트 (assertEquals, assertThat)
  • 예외가 발생하는 경우 → 예외 테스트 (assertThatThrownBy, assertThrows)
@Test
void 유저생성_성공() {
    when(userRepository.save(any(User.class))).thenReturn(mockUser);
    when(userStatusRepository.save(any(UserStatus.class))).thenReturn(mockUserStatus);
    when(userRepository.existsByUsername(createUserParam.username())).thenReturn(false);
    when(userRepository.existsByEmail(createUserParam.email())).thenReturn(false);

    UserDTO userDTO = basicUserService.create(createUserParam);

    assertEquals(createUserParam.username(), userDTO.username());
    assertEquals(createUserParam.email(), userDTO.email());

    verify(userRepository, times(1)).save(any(User.class));
}

6. 단위 테스트 철학

"단위 테스트는 내가 테스트하는 대상만 검증해야 한다. 다른 서비스나 메서드의 세부 동작까지 검증할 필요는 없다."

  • 예제: ChannelService에서 ReadStatusService를 검증할 필요 없음
@Test
void 비밀_채널생성_성공() {
    when(channelRepository.save(any(Channel.class))).thenReturn(mockChannel);
    when(readStatusService.create(any(CreateReadStatusParam.class))).thenReturn(mockReadStatus);

    ChannelDTO channelDTO = channelService.createPrivateChannel(userIds, createChannelParam);

    assertEquals(mockChannel.getId(), channelDTO.id());

    // ❌ `readStatusService.create()` 내부 로직을 검증할 필요 없음!
    // ✅ 몇 번 호출됐는지만 확인하면 충분함.
    verify(readStatusService, times(userIds.size())).create(any(CreateReadStatusParam.class));
}
  • 왜 readStatusService.create() 내부 동작까지 검증할 필요가 없을까?
  • ReadStatusService가 올바르게 동작하는지는 ReadStatusService의 테스트에서 검증하면 됨
  • ChannelService 테스트에서는 readStatusService.create()가 제대로 호출되었는지만 확인하면 충분
    → verify()를 사용하여 호출 횟수만 체크하면 됨

7. 최종 정리

  • Spring Boot Starter Test에 JUnit5, Mockito, AssertJ 포함
  • 비즈니스 로직에서 예외를 던지는 경우 반드시 테스트
  • 단순 Repository 호출은 verify()로 호출 여부만 검증
  • 다른 서비스의 동작까지 검증할 필요 없음 → verify()로 호출 횟수만 체크
  • "단위 테스트는 내가 테스트하는 대상만 검증"
profile
백엔드 개발자를 꿈꿉니다.

0개의 댓글