TIL - 20251127

juni·2025년 11월 26일

TIL

목록 보기
190/317

1127 Spring Boot 테스트 전략: 단위 테스트와 Mocking


✅ 1. 테스트의 종류와 테스트 피라미드

  • 소프트웨어 테스트는 검증하려는 범위와 목적에 따라 여러 종류로 나뉩니다. 테스트 피라미드는 안정적이고 효율적인 테스트 전략을 수립하기 위한 모델입니다.

    Test Pyramid

  1. 단위 테스트 (Unit Test) - 피라미드의 가장 넓은 기반:

    • 범위: 클래스나 메서드와 같은 가장 작은 코드 단위를 테스트합니다.
    • 특징: 외부 의존성(DB, 네트워크 등)을 격리(차단)하고, 오직 해당 단위의 로직만을 검증합니다. 실행 속도가 매우 빠르고, 안정적입니다.
    • 목표: 코드의 각 부분이 개별적으로 정확하게 동작하는지 확인합니다.
  2. 통합 테스트 (Integration Test) - 피라미드의 중간:

    • 범위: 여러 컴포넌트(e.g., 컨트롤러-서비스-리포지토리)나 외부 시스템(e.g., DB, 외부 API)이 함께 연동되는 과정을 테스트합니다.
    • 특징: 실제 환경과 유사하게 동작하지만, 외부 의존성으로 인해 실행 속도가 느리고 깨지기 쉽습니다.
    • 목표: 각 컴포넌트 간의 상호작용이 올바르게 이루어지는지 확인합니다. (@SpringBootTest가 대표적)
  3. E2E 테스트 (End-to-End Test) - 피라미드의 최상단:

    • 범위: 실제 사용자의 시나리오 전체를 처음부터 끝까지 테스트합니다. (e.g., UI 클릭 → API 요청 → DB 조회 → UI 렌더링)
    • 특징: 가장 신뢰도가 높지만, 구축이 복잡하고 실행 시간이 매우 깁니다.
  • 전략: 빠르고 안정적인 단위 테스트를 가장 많이 작성하고, 꼭 필요한 부분에 대해서만 통합 테스트와 E2E 테스트를 작성하여 효율적인 테스트 스위트를 구성해야 합니다.

✅ 2. 단위 테스트의 핵심: 의존성 격리와 Mocking

  • 문제점: UserServicecreateUser 메서드를 단위 테스트하고 싶다고 가정해봅시다. 이 메서드는 내부적으로 UserRepository에 의존하여 DB에 접근합니다. 만약 실제 UserRepository를 사용하면, 이는 DB라는 외부 의존성을 포함하게 되어 더 이상 순수한 단위 테스트가 아니게 됩니다. (DB 상태에 따라 테스트 결과가 달라질 수 있음)

  • 해결책 (Mocking): Mock(가짜) 객체를 사용하여 실제 의존성을 대체하는 기술입니다. Mock 객체는 실제 로직을 수행하지 않고, 우리가 "이렇게 동작해라"라고 미리 정의한 행동만 수행하는 껍데기 객체입니다.

➕ Mockito: Java의 대표적인 Mocking 프레임워크

  • Mockito는 Mock 객체를 쉽게 생성하고, 그 행동을 정의할 수 있도록 도와주는 라이브러리입니다. Spring Boot 테스트 스타터에 기본적으로 포함되어 있습니다.

  • 주요 Mockito 어노테이션 및 메서드:

    • @Mock: 가짜(Mock) 객체를 생성합니다. 이 객체는 비어있는 껍데기입니다.
    • @InjectMocks: 테스트 대상이 되는 객체를 생성하고, @Mock으로 생성된 가짜 객체들을 자동으로 주입해줍니다.
    • when(mockObject.method(argument)).thenReturn(returnValue): Mock 객체의 특정 메서드가 특정 인자로 호출될 때, 어떤 값을 반환할지를 정의(Stubbing)합니다.
    • verify(mockObject).method(argument): 테스트가 끝난 후, Mock 객체의 특정 메서드가 예상대로 호출되었는지를 검증합니다.

✅ 3. Spring Boot 단위 테스트 예시 (@ExtendWith(MockitoExtension.class))

  • @SpringBootTest 대신, Spring 컨테이너의 도움 없이 순수 JUnit과 Mockito만으로 서비스 계층을 테스트하는 방법입니다.
// Spring 컨테이너를 로드하지 않고, Mockito 기능을 활성화
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock // 가짜 UserRepository 생성
    private UserRepository userRepository;

    @InjectMocks // 테스트 대상인 UserService에 위의 가짜 userRepository를 주입
    private UserService userService;

    @Test
    @DisplayName("회원가입 성공 테스트")
    void signUp_Success() {
        // given - 테스트 준비
        SignUpRequest request = new SignUpRequest("test@example.com", "password123");
        User mockUser = new User(1L, "test@example.com", "encodedPassword");

        // userRepository.findByEmail()이 호출되면, 비어있는 Optional을 반환하도록 정의 (중복 이메일 없음)
        when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
        
        // userRepository.save()가 어떤 User 객체로든 호출되면, 미리 만들어둔 mockUser를 반환하도록 정의
        when(userRepository.save(any(User.class))).thenReturn(mockUser);

        // when - 테스트 실행
        User result = userService.signUp(request);

        // then - 결과 검증
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getEmail()).isEqualTo("test@example.com");

        // userRepository의 save 메서드가 정확히 1번 호출되었는지 검증
        verify(userRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("회원가입 실패 - 이메일 중복")
    void signUp_Fail_EmailDuplicated() {
        // given
        SignUpRequest request = new SignUpRequest("test@example.com", "password123");
        User existingUser = new User(1L, "test@example.com", "password");

        // userRepository.findByEmail()이 호출되면, 이미 존재하는 사용자를 반환하도록 정의
        when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.of(existingUser));

        // when & then
        // signUp 메서드가 BusinessException을 던지는지 검증
        assertThrows(BusinessException.class, () -> {
            userService.signUp(request);
        });

        // userRepository의 save 메서드가 절대 호출되지 않았는지 검증
        verify(userRepository, never()).save(any(User.class));
    }
}

📌 요약

  • 효율적인 테스트 전략은 빠르고 안정적인 단위 테스트를 기반으로, 필요한 부분에만 통합 테스트를 추가하는 테스트 피라미드 모델을 따릅니다.
  • 단위 테스트의 핵심은 테스트 대상을 외부 의존성으로부터 격리하는 것이며, 이를 위해 Mocking(모킹) 기술을 사용합니다.
  • Mockito@Mock으로 가짜 객체를 만들고, when().thenReturn()으로 행동을 정의(Stubbing)하며, verify()로 호출 여부를 검증하는 방식으로 단위 테스트를 작성할 수 있게 해주는 강력한 프레임워크입니다.
  • Spring 컨테이너의 도움 없이 @ExtendWith(MockitoExtension.class)를 사용하면, 서비스 계층과 같이 순수 Java 로직에 가까운 부분을 매우 가볍고 빠르게 테스트할 수 있습니다.

0개의 댓글