사용하는 라이브러리

  • Junit5
    • 자바 단위 테스트를 위한 테스트 프레임워크
  • AssertJ
    • 자바 테스트를 돕기 위해 다양한 문법을 지원하는 라이브러리

given when then

  • given
    • 데이터의 준비 과정
  • when
    • 주어진 데이터를 기반으로 동작을 진행했을 때
  • then
    • 동작 실행에 대한 Output 데이터 검증 부분

예시 - 퀴즈 데이터를 생성하는 것을 검증하기 위한 테스트이다.

// given  
User user = new User("nickname", "loginId", "password");  
this.userRepository.save(user);  
 
// 단어와 word_user 저장  
for (int i = 0; i < 20; i++) {  
	Word word = new Word("word" + i, "meaning");  
	this.wordRepository.save(word);  
	joinWordRepository.saveUserWord(user, word);  
}  
 
// when  
Long quizSetId = this.quizService.generateQuizSet(user.getId());  
QuizSet quizInfo = this.quizService.getQuizInfo(quizSetId);  
 
// then  
assertThat(quizInfo.getCurrentSequence()).isEqualTo(0);  
assertThat(quizInfo.getQuizDetails().size()).isEqualTo(10);  
assertThat(quizInfo.getUser().getId()).isEqualTo(user.getId());

Mockito

Mockito는 자바 언어를 사용하는 소프트웨어 개발자들이 단위 테스트를 작성하는 데 사용하는 오픈 소스 프레임워크이다. Mock이 필요한 테스트에 직관적으로 사용할 수 있도록 만들어졌습니다.

주요 어노테이션

  • Mock
    • Mock 객체를 생성할 때 사용한다.
  • MockBean
    • Spring에서 제공하는 Bean을 Mocking할 때 사용한다.
  • Spy
    • 실제 객체를 사용해서 테스트를 진행한다.
    • Stub 하지 않은 메소드들은 원본 그대로 사용한다.
  • InjectMocks
    • 주입할 대상 객체의 생성자를 호출하여 모의 객체를 자동으로 주입한다.

Stub이란

테스트 도중에 호출 될 메서드 들에 대해서 미리 답변을 준비해 놓는 것을 의미한다.
즉 mock객체의 메소드를 호출했을 때 어떤 값을 리턴할 지 미리 정해놓는 것이다.

주요 메서드

  • doReturn()
    • 특정 값을 반환해야 하는 경우
  • doNothing()
    • 아무 값도 반환하지 않는 경우
  • doThrow()
    • 예외를 발생시키는 경우

예제

CompanyResponseDto companyResponseDto = mock(CompanyResponseDto.class);
 
when(companyResponseData.getData()).thenReturn(companyResponseDto);
 
doReturn(new CommonResponse<RiderResponseDto>(200, "메시지", givenRider))  
    .when(riderClient).authRider(requestDto);

두 방식의 차이점

특징when(…).thenReturn(…)doReturn(…).when(…)
기본 사용 사례일반적인 메서드 Stub 처리예외를 방지하거나 final/static/private 메서드 처리
가독성상대적으로 간결하고 직관적약간 복잡하고 이해하기 어려울 수 있음
예외 발생 가능성실제 메서드 호출 시 예외 발생 가능메서드를 호출하지 않으므로 예외 발생하지 않음
사용 가능 범위일반 메서드final, private, void 메서드에도 사용 가능
실행 시점메서드가 실제 호출될 때 Stub 설정Stub 설정 시점에 메서드를 실행하지 않음

Spring에서 사용 방법

아래와 같이 테스트 할 클래스의 어노테이션으로 붙여주면 된다.

@ExtendWith(MockitoExtension.class)  
class 테스트_클래스_이름 {
	...
}

공통 적용 사항

테스트를 원하는 RiderAuthServic는 InjectsMock으로 주입해준다.
외부 서비스를 활용해야 하는 RiderClient 및 RedisUtils는 Mock을 이용한다.
다른 외부 서비스를 사용하지 않는 JwtTokenService는 Spy를 써서 기존 로직을 이용한다.
Inject될 Service에 생성자로 추가해줘야 할 데이터가 있어서 BeforeEach를 활용해서 셋업을 해준다.

@InjectMocks  
private RiderAuthService riderAuthService;  
  
@Mock  
private RiderClient riderClient;  
  
@Mock  
private RedisUtil redisUtil;  
  
@Spy  
private JwtTokenService jwtTokenService = new JwtTokenService(  
    "dGVzdE1vY2t0ZXN0TW9ja3Rlc3RNb2NrdGVzdE1vY2t0ZXN0TW9ja3Rlc3RNb2Nr", "3600");  
 
@BeforeEach  
void setUp() {  
    // `@Value`로 주입되는 값은 생성자로 전달  
    riderAuthService = new RiderAuthService(riderClient, redisUtil, jwtTokenService, "3600");  
  
}

given when then을 사용해서 작성한 성공 예제

라이더인 유저가 정상적으로 인증을 성공하는지 테스틑 하는 Flow이다. 서비스의 입력값과 내부로직 상 돌아갈 데이터를 Mock해서 given으로 처리해준다. 외부 호출을 진행하지 않을 2개의 메서드를 Mocking 처리 해준다. 이후 생성된 token값과 내부 함수 호출을 점검한다.

@DisplayName("라이더 인증 서비스 성공")  
@Test  
public void 인증_성공_테스트() {  
 
	String userId = "userId";  
	RiderAuthRequestDto requestDto = new RiderAuthRequestDto(userId, "password");  
	RefreshTokenDto refreshTokenDtoMock = mock(RefreshTokenDto.class);  
 
	// given  
	RiderResponseDto givenRider = new RiderResponseDto(  
		1L, userId, Arrays.asList(1L, 2L), RiderTransportation.MOTORCYCLE,  
		LocalDateTime.now());  
 
	doReturn(new CommonResponse<RiderResponseDto>(200, "메시지", givenRider))  
		.when(riderClient).authRider(requestDto);  
 
	doNothing().when(redisUtil).setDataRefreshToken(any(RefreshTokenDto.class));  
 
	// when  
	String token = riderAuthService.riderAuth(requestDto);  
	Claims claims = jwtTokenService.extractClaims(token);  
 
	// then  
	// token의 subject 확인  
	Assertions.assertThat(claims.getSubject()).isEqualTo(userId);  
 
	// 내부 함수 호출 횟수 확인  
	verify(redisUtil, times(1)).setDataRefreshToken(any(RefreshTokenDto.class));  
 
}
배열 포함 확인

특정 값이 배열에 포함되었는지 확인하기 위해서는 아래와 같이 확인할 수 있다.

// contains를 사용해서 Output값이 포함되었는지 여부 확인
assertThat(List.of(ownerOne, ownerTwo)).contains(stores.get(0).getOwnerId());  
 
// stream을 사용해서 하나씩 비교해가면서 Match 여부를 확인해 본다.
assertThat(Stream.of(ownerOne, ownerTwo)  
    .anyMatch(owenerId -> owenerId.equals(stores.get(0).getOwnerId())))
    .isTrue();

에러 처리 테스트

  1. assertThatThrownBy 사용

특정 메서드에 대해서 에러가 발생시키는 Mock을 만든다. 이후 ssertThatThrownBy를 사용해서 클래스의 타입과 메세지를 확인할 수 있다.

doThrow(new FeignException.NotFound("Rider not found", request, null, null)).when(  
    riderClient).authRider(requestDto);  
  
// then  
ssertThatThrownBy(() -> {  
	String token = riderAuthService.riderAuth(requestDto);  
})
.isInstanceOf(AuthException.class)  
.hasMessage(AuthErrorCode.USER_NOT_FOUND.getMessage());
  1. catchThrowable 사용

catchThrowable을 사용해서 에러를 먼저 받은 다음에 기존과 같은 방식으로 인스턴스와 메시지를 비교해준다.

doThrow(new FeignException.BadRequest("Invalid Password", request, null, null)).when(  
    riderClient).authRider(requestDto);  
  
Throwable error = Assertions.catchThrowable(() -> {  
    String token = riderAuthService.riderAuth(requestDto);  
});  
  
// then  
Assertions.assertThat(error)  
    .isInstanceOf(AuthException.class)  
    .hasMessage(AuthErrorCode.INVALID_PASSWORD.getMessage());
  1. assertThatExceptionOfType

Type을 사용해서 먼저 타입을 알려준 다음에 에러 발생 시 메시지 확인도 가능하다.

assertThatExceptionOfType(AuthException.class).isThrownBy(() -> {  
    String token = riderAuthService.riderAuth(requestDto);  
})
.withMessage(USER_NOT_FOUND.getMessage());

테스트 환경에서 변수 주입

Entity의 private 필드에 접속하기

  • id 필드를 갖고 와서 특정 값을 주입해 주는 방식으로 메소드를 만든다.
private void setEntityId(Object entity, Long id) 
	throws NoSuchFieldException, IllegalAccessException {
	
    Field idField = entity.getClass().getDeclaredField("id");  
    idField.setAccessible(true);  
    idField.set(entity, id);  
}
  • 아래와 같이 entity를 생성 후 id를 주입하면 된다.
setEntityId(user1, 1L);

상속받는 변수의 경우에서 처리 방법

  • 위와 같이 entity.getClass()의 경우 상속받는 변수에 대해서는 처리가 불가능하다.
private void setEntityCreateTime(Object entity, LocalDateTime createdAt) {  
    Field field = ReflectionUtils.findField(entity.getClass(), "createdAt");  
    if (field != null) {  
        field.setAccessible(true);  
        ReflectionUtils.setField(field, entity, createdAt);  
    }  
}
  • 위와 같이 ReflectionUtils을 사용해서 field를 찾아서 추가하면 된다.
setEntityCreateTime(logFirst, LocalDateTime.now());