단위 테스트 적용하기

lv2dev·2024년 2월 2일
0

테스트는 중대사항이다. 코드의 품질 보증을 할 수 있고, 안정적인 배포에 큰 기여를 한다. 기존 프로젝트에서 시간이 촉박하다는 이유로 이 테스트 파트를 수행하지 않고 진행했더니 디버깅 시간이 너무 길어졌고 실제로 돌릴때도 예상치 못한 오류가 너무 많이 발생했다. 그래서 다음 프로젝트 부터는 꼭 단위테스트를 수행하자고 생각했다.

일단 나는 방금 만든 간단한 회원가입 코드를 테스트 할 수 있도록 해 보았다.

Build.bradle

testImplementation 'org.springframework.boot:spring-boot-starter-test'

MemberServiceTest.java

// Spring 환경에서 JUnit 5를 사용하여 테스트를 수행하는 설정입니다. SpringExtension은 Spring TestContext Framework를 JUnit 5 실행과 통합합니다.
@ExtendWith(SpringExtension.class)
// SpringBootTest 어노테이션은 스프링 부트 테스트를 위한 전체 애플리케이션 컨텍스트를 로드합니다. 이를 통해 실제 애플리케이션 환경과 유사한 테스트 환경을 구성할 수 있습니다.
@SpringBootTest
public class MemberServiceTest {

    // MemberRepository의 모의 객체를 생성합니다. @MockBean을 사용하면 Spring 컨텍스트에 해당 모의 객체가 등록되며, 이 객체는 테스트 중에 @Autowired로 주입받는 모든 곳에 사용됩니다.
    @MockBean
    private MemberRepository memberRepository;

    // PasswordEncoder의 모의 객체를 생성합니다. 비밀번호 인코딩 과정을 실제로 수행하지 않고 모의 동작을 정의할 수 있습니다.
    @MockBean
    private PasswordEncoder passwordEncoder;

    // S3Service의 모의 객체를 생성합니다. 파일 업로드 등의 외부 서비스 호출을 실제로 수행하지 않고 모의 동작을 정의할 수 있습니다.
    @MockBean
    private S3Service s3Service;

    // MemberService에 대한 인스턴스를 생성하고, 위에서 정의한 모의 객체들을 자동으로 주입합니다. @InjectMocks는 Mockito에서 모의 객체를 테스트 대상 객체에 주입할 때 사용됩니다.
    @Autowired
    @InjectMocks
    private MemberService memberService;

    // 회원가입 성공 시나리오를 테스트합니다. 모든 입력 조건이 충족되었을 때, MemberRepository의 save 메소드가 호출되는지 검증합니다.
    @Test
    public void signUp_Success() throws IOException {
        // Given: 테스트를 위한 사전 조건 설정
        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setEmail("test@example.com");
        memberDTO.setNickname("testUser");
        memberDTO.setPassword("Password@123");

        // 모의 객체에 대한 동작을 정의합니다. 이메일 또는 닉네임이 중복되지 않음을 반환하고, 비밀번호 인코딩 결과를 설정합니다.
        when(memberRepository.existsByEmail(anyString())).thenReturn(false);
        when(memberRepository.existsByNickname(anyString())).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");

        // When: 실제 테스트 대상 메소드를 실행
        memberService.signUp(memberDTO);

        // Then: 기대 결과 검증. MemberRepository의 save 메소드가 정확히 한 번 호출되었는지 확인합니다.
        verify(memberRepository, times(1)).save(any(Member.class));
    }
    
    @Test
public void signUp_Success_WithProfileImage() throws IOException {
    // Given
    MemberDTO memberDTO = new MemberDTO();
    memberDTO.setEmail("testwithimage@example.com");
    memberDTO.setNickname("testUserImage");
    memberDTO.setPassword("Password@123");
    
    // 프로필 이미지를 위한 MockMultipartFile 객체 생성
    MultipartFile profileImage = new MockMultipartFile(
        "profile", // 파일 파라미터 이름
        "profile.jpg", // 파일 이름
        "image/jpeg", // 파일 타입
        "<<jpeg data>>".getBytes() // 파일 내용
    );
    memberDTO.setProfile(profileImage);

    // 모의 객체 설정
    when(memberRepository.existsByEmail(anyString())).thenReturn(false);
    when(memberRepository.existsByNickname(anyString())).thenReturn(false);
    when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
    // S3Service를 통해 파일 업로드를 시뮬레이션합니다. 업로드된 파일 URL을 반환하도록 설정합니다.
    when(s3Service.uploadFile(any(MultipartFile.class), anyString(), anyString())).thenReturn("http://example.com/profile.jpg");

    // When
    memberService.signUp(memberDTO);

    // Then
    // Member 객체가 저장되었는지 확인합니다. 이때, 프로필 이미지 URL이 설정된 Member 객체가 저장되었는지 검증할 수 있습니다.
    verify(memberRepository, times(1)).save(any(Member.class));
    // S3Service의 uploadFile 메소드가 호출되었는지 확인합니다.
    verify(s3Service, times(1)).uploadFile(any(MultipartFile.class), anyString(), anyString());
}

    // 이메일이 이미 존재하는 경우의 회원가입 실패 시나리오를 테스트합니다. 이메일 중복 시 ResponseStatusException 예외가 발생하는지 검증합니다.
    @Test
    public void signUp_Fail_IfEmailExists() {
        // Given: 테스트를 위한 사전 조건 설정
        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setEmail("existing@example.com");
        memberDTO.setNickname("testUser");
        memberDTO.setPassword("Password@123");

        // 이메일이 이미 존재한다고 설정합니다.
        when(memberRepository.existsByEmail(anyString())).thenReturn(true);

        // When & Then: signUp 메소드 실행 시 ResponseStatusException이 발생하는지 검증합니다.
        assertThrows(ResponseStatusException.class, () -> memberService.signUp(memberDTO));
    }
}

gpt의 주석은 최강이다...

실행(Gradle)

./gradlew test

특정 테스트 클래스만 실행하려면 아래와 같이 사용한다.

./gradlew test --tests "com.example.project.service.MemberServiceTest"

결과

실패한 모습

성공한 모습

더 알아보기

JUnit?

  • Java에서 독립된 단위 테스트를 지원해주는 프레임워크

Spring Boot Test

  • Spring Boot 에서 제공하는 기본적인 테스트 스타터
  • JUnit뿐만 아니라 Java 계열의 언어에서 사용하는 테스트 라이브러리들이 한데 모여있다.

@MockBean

  • Spring Boot의 테스트 유틸리티 중 하나로 Spring의 애플리케이션 컨텍스트에 모의객체(Mock Object)를 추가하거나 기존의 Bean을 모의 객체로 대체하는데 사용된다.
  • @MockBean으로 생성된 모의 객체는 테스트 실행 시 Spring 컨텍스트에 자동으로 주입되어, 애플리케이션의 다른 부분은 실제 구현 대신 이 모의 객체와 상호작용하게 된다.
  • 데이터베이스, 외부 서비스 호출 등의 실제 작업을 수행하지 않으므로, 테스트 실행 속도를 향상시킬 수 있다.
  • 테스트 실행 시 기존의 Spring Bean을 모의 객체로 대체하므로, 테스트 간에 빈의 상태가 유지되지 않아 각 테스트가 독립적으로 실행된다.

MockMultipartFile

  • Spring에서 제공하는 클래스로 주로 파일 업로드를 테스트 할 때 사용된다.
  • MultipartFile 인터페이스의 구현체로, 테스트 환경에서 파일 업로드의 Mock을 생성할 수 있게 해준다.

Mockito

  • 자바 기반의 테스팅 프레임워크
  • 주로 단위 테스트에서 객체의 모의 구현(mock implementations)을 만들어 사용하는 데 사용된다.
  • 이를 통해 테스트하려는 코드의 의존성을 간단하게 제거하고, 테스트 대상 코드가 예상대로 동작하는지 검증할 수 있다.
  • Mockito는 객체의 실제 구현 대신에 가짜 구현을 주입함으로써, 테스트 대상 코드가 예상대로 동작하는지 검증할 수 있다.

Mockito의 주요기능

  • Mock 객체 생성 mock() 메서드를 사용해 클래스나 인터페이스의 모의 객체 생성
  • stub 메서드 호출 : when()thenReturn()을 사용하여 모의 객체의 메서드 호출에 대한 응답을 정의할 수 있다. 이를 통해 특정 메서드가 호출될 때 원하는 값을 반환하거나 예외를 발생시킬 수 있다.
  • 메서드 호출 검증 : verify() 메서드를 사용해 특정 메서드가 특정 인자로 몇 번 호출되었는지 검증할 수 있다. 테스트 대상 코드가 의존성과 올바르게 상호작용 하는지 확인하는 데 유용하다.
  • 인자 캡쳐 : ArgumentCaptor를 사용해 메서드 호출 시 전달된 인자를 캡쳐하고 검증할 수 있다. 이를 통해 메서드가 예상대로의 인자로 호출되었는지 확인할 수 있다.
  • spy 객체 : 실제 객체의 일부 메서드를 모의로 대체하면서 나머지 실제 동작을 유지하고 싶을 때 사용한다. spy() 메서드를 통해 생성된 스파이 객체는 대부분 실제 로직을 실행하지만, 필요한 부분만 모의로 대체할 수 있다.

stub?

  • 실제 구현 대신에 테스트 대상의 의존성을 대체하기 위해 사용되는 간단한 구현 또는 코드 조각
  • 테스트하려는 코드가 외부 시스템, 서비스, 또는 복잡한 모듈과 상호작용할 때, 이러한 외부 의존성을 모의하는 데 사용된다.
  • 단순성, 입력과 응답을 정확히 제어가능, 독립성

Mock 사용예

import static org.mockito.Mockito.*;

// 모의 객체 생성
List mockedList = mock(List.class);

// 스텁 설정
when(mockedList.get(0)).thenReturn("first");

// 모의 객체 사용
System.out.println(mockedList.get(0)); // "first" 출력

// 메서드 호출 검증
verify(mockedList).get(0);

// 스파이 객체 생성 및 사용
List list = new LinkedList();
List spy = spy(list);

// "one"이라는 요소 추가는 실제로 실행됩니다.
spy.add("one");
// "one"이라는 요소가 존재하는지 확인하는 것도 실제 메서드를 호출합니다.
verify(spy).add("one");
// 하지만 get(0) 메서드 호출 시 "first"를 반환하도록 스텁을 설정할 수 있습니다.
when(spy.get(0)).thenReturn("first");

참고

profile
언제나 레벨업을 하고 싶은 영원한 lv1

0개의 댓글