테스트는 중대사항이다. 코드의 품질 보증을 할 수 있고, 안정적인 배포에 큰 기여를 한다. 기존 프로젝트에서 시간이 촉박하다는 이유로 이 테스트 파트를 수행하지 않고 진행했더니 디버깅 시간이 너무 길어졌고 실제로 돌릴때도 예상치 못한 오류가 너무 많이 발생했다. 그래서 다음 프로젝트 부터는 꼭 단위테스트를 수행하자고 생각했다.
일단 나는 방금 만든 간단한 회원가입 코드를 테스트 할 수 있도록 해 보았다.
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 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의 주석은 최강이다...
./gradlew test
특정 테스트 클래스만 실행하려면 아래와 같이 사용한다.
./gradlew test --tests "com.example.project.service.MemberServiceTest"
실패한 모습
성공한 모습
mock()
메서드를 사용해 클래스나 인터페이스의 모의 객체 생성when()
과 thenReturn()
을 사용하여 모의 객체의 메서드 호출에 대한 응답을 정의할 수 있다. 이를 통해 특정 메서드가 호출될 때 원하는 값을 반환하거나 예외를 발생시킬 수 있다.verify()
메서드를 사용해 특정 메서드가 특정 인자로 몇 번 호출되었는지 검증할 수 있다. 테스트 대상 코드가 의존성과 올바르게 상호작용 하는지 확인하는 데 유용하다.ArgumentCaptor
를 사용해 메서드 호출 시 전달된 인자를 캡쳐하고 검증할 수 있다. 이를 통해 메서드가 예상대로의 인자로 호출되었는지 확인할 수 있다.spy()
메서드를 통해 생성된 스파이 객체는 대부분 실제 로직을 실행하지만, 필요한 부분만 모의로 대체할 수 있다.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");