스프링부트 + 자바 환경이라면 Mockito의 @Mock, @InjectMocks 어노테이션으로 모킹과 주입을 손쉽게 할수 있다.
하지만 스프링부트 + 코틀린 환경에서 자바 환경에서 처럼 Mockito 를 사용하면 많은 오류를 만날 수 있는데 대표적으로 lateinit val를 활용함에 있어 발생하는 의존성 주입이 제대로 되지 않는 문제, 목 객체가 제대로 주입이 되지 않는 등 테스트 시작전에 에러를 내뿜는다.
이런 문제 때문에 스프링부트 + 코틀린 환경에 특화된 여러 라이브러리(Mockk)가 존재하지만 대부분 코틀린 DSL을 활용하기도 하고 새롭게 익히기 귀찮아서 코틀린과 Mockito를 같이 사용하는 가장 깔끔하다고 생각하는 스타일을 공유한다.
@ExtendWith(MockitoExtension::class)
@DataRedisTest
class LoginServiceTest(
@Autowired private val redisTemplate: RedisTemplate<String, String>
) {
// 어노테이션이 아닌 메소드를 통한 목 객체 생성
val jwtTokenProvider: JwtTokenProvider = Mockito.mock(JwtTokenProvider::class.java)
// 스프링의 의존성 주입을 사용하지 않고 직접 의존성을 주입하기
val loginService: LoginService = LoginService(jwtTokenProvider, redisTemplate)
// given
val accessToken = "access-token"
val invalidAccessToken = "invalid-access-token"
val reissuedAccessToken = "reissue-access-token"
val refreshToken = "refresh-token"
val email = "test@test.com"
val hashOps = redisTemplate.opsForHash<String, String>()
val key = "auth:login:${email}"
@Nested
inner class SaveTokenAtCacheTest {
@Test
@DisplayName("인증 토큰이 레디스 캐시에 저장되어야 한다.")
fun saveTokenAtCache_shouldSuccess(){
// stubbing
Mockito.`when`(jwtTokenProvider.getEmailAddress(accessToken)).thenReturn(email)
// when
loginService.saveTokenAtCache(accessToken, refreshToken)
// then
assertThat(hashOps.get(key, "accessToken")).isEqualTo(accessToken)
assertThat(hashOps.get(key, "refreshToken")).isEqualTo(refreshToken)
assertThat(redisTemplate.getExpire(key, TimeUnit.DAYS)).isLessThanOrEqualTo(14)
// tear down
redisTemplate.delete(key)
}
}
@Nested
inner class ValidateAccessTokenTest {
@BeforeEach
fun setup() {
hashOps.putAll(key, hashMapOf(
"accessToken" to accessToken,
"refreshToken" to refreshToken
))
}
@AfterEach
fun tearDown(){
redisTemplate.delete(key)
}
@Test
@DisplayName("저장된 RefreshToken 이 만료되었다면 ExpiredRefreshTokenException 을 던져야 한다.")
fun validateAccessToken_shouldThrowExpiredRefreshTokenException(){
// stubbing
Mockito.`when`(jwtTokenProvider.validateToken(refreshToken)).thenReturn(false)
// when, then
assertThrows<ExpiredRefreshTokenException> {
loginService.validateAccessToken(accessToken, refreshToken, key)
}
}
@Test
@DisplayName("AccessToken 이 저장된 AccessToken 과 다르면 InvalidAccessTokenException 을 던져야 한다.")
fun validateAccessToken_shouldThrowExpiredInvalidAccessTokenException(){
// stubbing
Mockito.`when`(jwtTokenProvider.validateToken(refreshToken)).thenReturn(true)
// when, then
assertThrows<InvalidAccessTokenException> {
loginService.validateAccessToken(invalidAccessToken, refreshToken, key)
}
}
}
@Nested
inner class ReissueAccessTokenTest {
@BeforeEach
fun setup() {
hashOps.putAll(key, hashMapOf(
"accessToken" to accessToken,
"refreshToken" to refreshToken
))
// stubbing
Mockito.`when`(jwtTokenProvider.getEmailAddress(Mockito.anyString())).thenReturn(email)
}
@AfterEach
fun tearDown(){
redisTemplate.delete(key)
}
@Test
@DisplayName("AccessToken 재발급이 성공해야 한다.")
fun reissueAccessToken_shouldSuccess() {
// given
val authentication = UsernamePasswordAuthenticationToken("","")
// stubbing
Mockito.`when`(jwtTokenProvider.validateToken(refreshToken)).thenReturn(true)
Mockito.`when`(jwtTokenProvider.getAuthentication(accessToken)).thenReturn(authentication)
Mockito.`when`(jwtTokenProvider.createToken(authentication, TokenType.ACCESS_TOKEN)).thenReturn(reissuedAccessToken)
// when
val result = loginService.reissueAccessToken(accessToken, refreshToken)
assertThat(result).isEqualTo(reissuedAccessToken)
assertThat(hashOps.get(key, "accessToken")).isEqualTo(reissuedAccessToken)
}
}
도움이 되었습니다.