[SpringBoot] 코틀린으로 단위 테스트 코드 작성시 유의점

tkppp·2022년 3월 15일

스프링부트 + 자바 환경이라면 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)
        }
    }

1개의 댓글

comment-user-thumbnail
2024년 1월 11일

도움이 되었습니다.

답글 달기