@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을 유효 기간과 함께 저장. 유효 기관을 초과하면 사라진다.@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);
}
}
returnTokenDto
객체를 생성했다.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);
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.
- 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
메소드가 실행되는 것을 확인할 수 있었다.
@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