트러블 슈팅 221103

u-nij·2022년 11월 8일
0

트러블 슈팅

목록 보기
6/6
post-thumbnail
post-custom-banner

실행 환경

  • Spring Boot 2.7.3
  • Java 11.0.9
  • JUnit5
  • Mockito

상황

  • 우선, 작업이 촉박하게 이루어지는 상황이라 테스트 코드를 작성하지 못했다.. 이미 로직을 작성해둔 상태라서 로직에 맞춰서 테스트 코드를 작업하려고 했다. 일단, 좋은 테스트를 위한 FIRST 규칙의 Timely는 지키지 못했다.
  • JWT 토큰을 발급해주는 메서드의 테스트 코드를 작성 중이었다.

AuthService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {

    private final JwtTokenProvider jwtTokenProvider;
    private final RedisService redisService;
    
    public TokenDto generateToken(String email, String authorities) {
        TokenDto tokenDto = jwtTokenProvider.createToken(email, authorities);
        saveRefreshToken(email, tokenDto.getRefreshToken()); // // line 78
        return tokenDto;
    }

    public void saveRefreshToken(String email, String refreshToken) {
        redisService.setValuesWithTimeout("RT(" + email + "):", // key
                refreshToken, // value
                jwtTokenProvider.getTokenExpirationTime(refreshToken)); // timeout(milliseconds)
    }
}
  • generateToken: Access Token과 Refresh Token을 생성하고, Redis에 Refresh Token을 저장
  • saveRefreshToken: Redis에 Refresh Token을 유효 기간과 함께 저장. 유효 기관을 초과하면 사라진다.

AuthServiceTest

@ExtendWith(MockitoExtension.class)
public class AuthServiceTest {

    @Mock
    JwtTokenProvider jwtTokenProvider;
    @Mock
    RedisService redisService;
    @Spy
    @InjectMocks
    AuthService authService;
    
    @Test
    public void generateToken() throws Exception {
        // given
        String email = "user@email.com";
        String authorities = "ROLE_USER";

        TokenDto returnTokenDto = new TokenDto("accessToken", "refreshToken");
        
        Mockito.when(authService.generateToken(email, authorities))
                .thenReturn(returnTokenDto);

        // when
        TokenDto generateToken = authService.generateToken(email, authorities); // line 72

        // then
        assertEquals(generateToken, returnTokenDto);
    }
}
  • 같은 TokenDto를 가리키게 하기 위해 returnTokenDto 객체를 생성했다.

발생한 문제 & 해결 방법

1. NullPointerException

java.lang.NullPointerException
at {프로젝트명}.Service.AuthService.generateToken(AuthService.java:78)
at {프로젝트명}.Service.AuthService.generateToken(AuthServiceTest.java:72)

왜?

디버깅을 해보니 tokenDto가 비어있었는데, JwtTokenProvider가 Mock 객체임을 떠올렸다.
AuthServiceTest에 아래의 코드를 추가했다.

Mockito.when(jwtTokenProvider.createToken(email, authorities))
       .thenReturn(returnTokenDto);

2. CannotStubVoidMethodWithReturnValue

org.mockito.exceptions.misusing.CannotStubVoidMethodWithReturnValue:
'setValuesWithTimeout' is a void method and it cannot be stubbed with a return value!
Voids are usually stubbed with Throwables:
doThrow(exception).when(mock).someVoidMethod();
If you need to set the void method to do nothing you can use:
doNothing().when(mock).someVoidMethod();

For more information, check out the javadocs for Mockito.doNothing().


If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. The method you are trying to stub is overloaded. Make sure you are calling the right overloaded version.
2. Somewhere in your test you are stubbing final methods. Sorry, Mockito does not verify/stub final methods.
3. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -

  • with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
  1. Mocking methods declared on non-public parent classes is not supported.

왜?

saveRefreshToken 메소드의 redisService.setValuesWithTimeout 메소드가 void 값을 리턴하는 메소드이기 때문에, 반환 값을 가지는 stub 방식을 사용할 수 없다고 한다.
메소드를 선택적으로 stub할 수 있도록 하는 @Spy 어노테이션과 해결책으로 제시한 doNothing().when(mock).someVoidMethod();을 사용해 해결했다. AuthService를 Spy 객체로 만들고, 아래와 같이 saveRefreshToken에 대한 stub을 작성했다.

Mockito.doNothing()
        .when(authService)
        .saveRefreshToken(email, returnTokenDto.getRefreshToken());

위의 코드를 추가하고 디버깅해보았더니, 실제 코드를 실행해보았더니 generateToken을 실행시켰을 때 AuthService saveRefreshToken을 호출하지 않고, stub으로 작성한 Spy 객체의 saveRefreshToken 메소드가 실행되는 것을 확인할 수 있었다.

최종 코드

AuthServiceTest

@ExtendWith(MockitoExtension.class)
public class AuthServiceTest {

    @Mock JwtTokenProvider jwtTokenProvider;
    @Mock RedisService redisService;

	@Spy
    @InjectMocks
    AuthService authService;

    @Test
    public void generateToken() throws Exception {
        // given
        String provider = SERVER;
        String email = "user@email.com";
        String authorities = "ROLE_USER";

        TokenDto returnTokenDto = new TokenDto("at", "rt");

        Mockito.when(jwtTokenProvider.createToken(email, authorities))
                .thenReturn(returnTokenDto);
        Mockito.doNothing()
                .when(authService)
                .saveRefreshToken(email, returnTokenDto.getRefreshToken());
        Mockito.when(authService.generateToken(email, authorities))
                .thenReturn(returnTokenDto);

        // when
        TokenDto generateToken = authService.generateToken(email, authorities);

        // then
        assertEquals(generateToken, returnTokenDto);
    }
}

실행 결과

한 3일동안 해결하지 못했던 문제였어서 '시간도 없는데.. 통합테스트로 바꿔버릴까..' 생각도 잠깐 들었지만🙄 단위 테스트 코드를 꼭 한 번 작성해보고 싶었다!! 에러를 해결하지 못했을 때, 실제 AuthService의 saveRefreshToken 메소드를 호출하는 것을 보면서 '이 방식이 단위 테스트가 맞나..?' 했는데, 결과적으로 Spy 객체를 사용함으로써 단위 테스트의 의미를 퇴색시키지 않은 것 같아 다행이다.

참고

https://jojoldu.tistory.com/239
https://stackoverflow.com/questions/33124153/mockito-nullpointerexception-when-stubbing-method
https://cobbybb.tistory.com/16

profile
삶은 달걀이다
post-custom-banner

0개의 댓글