테스팅

전성수·2025년 5월 31일

네, 일반적으로 단위 테스트를 위한 클래스와 통합 테스트를 위한 클래스는 따로 두는 편입니다. 이것은 테스트 전략에서 권장되는 모범 사례 중 하나입니다.

분리하는 이유
테스트의 목적 및 범위 명확화:

단위 테스트: 개별 코드 단위(메서드, 클래스)의 독립적인 기능이 올바르게 작동하는지 확인하는 데 집중합니다. 외부 의존성은 Mocking합니다.
통합 테스트: 여러 단위(클래스, 모듈)가 서로 연동하여 올바르게 작동하는지 확인하는 데 집중합니다. 실제 의존성(DB, 외부 API 등)과의 연동을 포함합니다.
이렇게 목적과 범위가 명확해지면 테스트 코드의 가독성과 유지보수성이 높아집니다.
테스트 실행 속도:

단위 테스트: Mocking을 사용하므로 Spring 컨텍스트 로딩이나 외부 시스템 연동이 없어 매우 빠르게 실행됩니다. 개발 중 자주, 그리고 많이 실행되어야 합니다.
통합 테스트: Spring 컨텍스트를 로드하고, 실제 데이터베이스나 메시지 큐 등 외부 시스템과 연동해야 하므로 단위 테스트보다 훨씬 느립니다. 자주 실행하기는 부담스러울 수 있습니다.
테스트 유형에 따라 실행 속도가 다르기 때문에, 개발자는 빠른 피드백을 위해 단위 테스트를 우선적으로 실행하고, 필요할 때 통합 테스트를 실행할 수 있도록 분리하는 것이 효율적입니다.
테스트 환경 설정의 복잡성:

단위 테스트: 간단한 JUnit, Mockito 설정만으로 충분한 경우가 많습니다.
통합 테스트: @SpringBootTest, @EmbeddedKafka, Testcontainers 등 복잡한 Spring 환경 설정이나 외부 시스템 연동 설정이 필요합니다.
하나의 테스트 클래스에 이 모든 설정을 혼합하면 코드가 지저분해지고 관리하기 어려워집니다.
클린 코드 및 가독성:

단위 테스트와 통합 테스트는 설정 방식, Mocking 여부, 검증 방식 등에서 차이가 큽니다. 이를 한 파일에 섞어두면 코드를 읽고 이해하기 어려워집니다.
각 테스트 유형에 맞는 전용 클래스를 사용함으로써 코드를 깔끔하게 유지하고, 어떤 유형의 테스트인지 쉽게 파악할 수 있습니다.
일반적인 파일/패키지 구조
Spring Boot 프로젝트에서는 src/test/java 아래에 다음과 같은 패키지 구조를 사용하여 테스트를 분리하는 것이 일반적입니다.

src/test/java
└── com/example/myproject
    └── service
        ├── UserServiceTest.java           // UserService에 대한 단위 테스트 (Mocking 위주)
        └── UserServiceIntegrationTest.java // UserService가 다른 컴포넌트(예: UserRepository)와 연동되는 통합 테스트

    └── controller
        ├── MemberControllerTest.java      // MemberController에 대한 단위 테스트 (직접 인스턴스화 또는 최소 Spring 컨텍스트)
        └── MemberControllerWebMvcTest.java // MemberController에 대한 웹 계층 통합 테스트 (@WebMvcTest)

    └── repository
        └── MemberRepositoryTest.java      // MemberRepository에 대한 단위 테스트 (DB 연동 없이 로직만)
        └── MemberRepositoryIntegrationTest.java // MemberRepository의 실제 DB 연동 통합 테스트 (@DataJpaTest)

    └── kafka
        └── KafkaProducerServiceTest.java       // KafkaProducerService 단위 테스트 (KafkaTemplate Mocking)
        └── KafkaProducerServiceIntegrationTest.java // KafkaProducerService 통합 테스트

또는 
src/test/java
└── com/example/myproject
    └── unit
        └── service
            └── UserServiceTest.java
        └── controller
            └── MemberControllerTest.java
        └── kafka
            └── KafkaProducerServiceTest.java
    └── integration
        └── service
            └── UserServiceIntegrationTest.java
        └── controller
            └── MemberControllerWebMvcTest.java
        └── repository
            └── MemberRepositoryIntegrationTest.java
        └── kafka
            └── KafkaProducerServiceIntegrationTest.java
@ExtendWith(MockitoExtension.class) // JUnit5에서 Mockito 사용을 위함
class MemberServiceTest {
    @InjectMocks private MemberService memberService; // 테스트 대상 서비스
    @Mock private MemberRepository memberRepository; // 레포지토리 Mocking
    @Spy private BCryptPasswordEncoder passwordEncoder; // 실제 암호화 로직을 사용하기 위해 Spy 사용 [14]

    @DisplayName("회원가입 성공: 유효한 아이디와 비밀번호")
    @Test
    void signUp_Successful_When_ValidRequestProvided() {
        // Given
        String username = "newuser";
        String password = "newpassword123";
        SignUpRequestDto requestDto = new SignUpRequestDto(username, password);

        // 중복 회원 없음으로 Stubbing
        when(memberRepository.findByUsername(username)).thenReturn(Optional.empty());

        // save 호출 시 Member 객체 반환하도록 Stubbing (id는 테스트에서 중요하지 않으므로 생략)
        Member savedMember = new Member(username, passwordEncoder.encode(password));
        when(memberRepository.save(any(Member.class))).thenReturn(savedMember);

        // When
        MemberResponseDto response = memberService.signUp(requestDto);

        // Then
        assertThat(response.getUsername()).isEqualTo(username);
        // 실제 암호화된 비밀번호와 요청 비밀번호가 일치하는지 확인
        assertThat(passwordEncoder.matches(password, response.getPassword())).isTrue();

        // Verify
        verify(memberRepository, times(1)).findByUsername(username); // 중복 검사 호출 확인
        verify(passwordEncoder, times(1)).encode(password); // encode 메서드가 1번 호출되었는지 검증
        verify(memberRepository, times(1)).save(any(Member.class)); // save 메서드가 1번 호출되었는지 검증
    }
}
@DisplayName("회원가입 실패: 중복된 아이디 존재 시 예외 발생")
@Test
void signUp_ThrowsException_When_DuplicateUsername() {
    // Given
    String username = "existinguser";
    String password = "password123";
    SignUpRequestDto requestDto = new SignUpRequestDto(username, password);

    // memberRepository.findByUsername() 호출 시 이미 존재하는 회원 반환하도록 Stubbing
    when(memberRepository.findByUsername(username)).thenReturn(Optional.of(new Member(username, "hashedpassword")));

    // When & Then
    // assertThrows를 사용하여 특정 예외가 발생하는지 검증 [5, 11]
    IllegalStateException thrown = assertThrows(
        IllegalStateException.class,
        () -> memberService.signUp(requestDto),
        "이미 존재하는 회원입니다." // 예상되는 예외 메시지
    );
    assertThat(thrown.getMessage()).contains("이미 존재하는 회원입니다.");

    // Verify: save 메서드는 호출되지 않아야 함
    verify(memberRepository, times(1)).findByUsername(username); // 중복 검사 호출 확인
    verify(memberRepository, never()).save(any(Member.class)); // save 메서드는 호출되지 않았는지 검증
    verify(passwordEncoder, never()).encode(any(String.class)); // encode 메서드도 호출되지 않았는지 검증
}
spring_blog/src/test/resources/junit-platform.properties


junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1
profile
ㅡ/ㅡ

0개의 댓글