티스토리 뷰

반응형

문제 상황


mockito-kotlin을 이용해 findByIdOrNull() 메서드를 모킹해서 테스트 코드를 작성중이었다.

@Test
fun `쿠폰 한건 조회`() {
    whenever(mockCouponRepository.findByIdOrNull(1L)).thenReturn(
        Coupon(id = 1L, type = CouponType.PERCENT, amount = 10, startAt = LocalDate.of(2023, 4, 12), endAt = LocalDate.of(2023, 4, 12))
    )

    val result = couponService.getOne(1L)

    assertThat(result.type).isEqualTo(CouponType.PERCENT)
    assertThat(result.amount).isEqualTo(10)
    assertThat(result.startAt).isEqualTo(LocalDate.of(2023, 4, 12))
    assertThat(result.endAt).isEqualTo(LocalDate.of(2023, 4, 12))
}

테스트 코드를 작성 후 테스트를 실행하니 WrongTypeOfReturnValue 에러가 발생하였다.

 

 

에러 내용

에러 메시지를 읽어 보면

첫번째의 경우 멀티 쓰레드를 사용하는 코드가 아니라 나에게는 해당되는 내용은 아닌것 처럼 보였다.

두번째의 경우 spy() 문법을 사용하거나 doReturn을 사용하라고 하여서 doReturn으로 변경해 에러를 해결해 보고자 하였다.

 

doReturn으로 변경

doReturn(
    Coupon(id = 1L, type = CouponType.PERCENT, amount = 10, startAt = LocalDate.of(2023, 4, 12), endAt = LocalDate.of(2023, 4, 12))
).whenever(mockCouponRepository).findByIdOrNull(1L)

thenReturn -> doReturn 으로 변경하면 해결될거라는 나의 기대와 달리 여전히 동일한 에러가 발생했다 :(

 

 

임시방편 문제 해결


그래서 에러 문구를 다시 천천히 읽어보니 Coupon cannot be returned by findById() 이라는 문구가 보였다.

 

나는 findByIdOrNull() 메서드를 스텁(stub)하였는데 Mockito는 왜 findById()를 에러메시지에 보여주고 있을까라는 의문이 들었다.

일단 잘 모르겠으니 findByIdOrNull()메서드를 살펴보자.

 

 

findByIdOrNull()

fun <T, ID> CrudRepository<T, ID>.findByIdOrNull(id: ID): T? = findById(id).orElse(null)

findByIdOrNull()은 기존의 findById()메서드에 orElse()를 추가한 확장함수이다.

 

여기서 문득 코틀린의 확장함수는 스텁(stub)이 안되는 걸까 라는 의문이 들었다.

 

그래서 먼저 확장함수가 아닌 findById() 메서드를 스텁 하는 방식으로 변경해보았다.

whenever(mockCouponRepository.findById(1L)).thenReturn(
    Optional.of(Coupon(id = 1L, type = CouponType.PERCENT, amount = 10, startAt = LocalDate.of(2023, 4, 12), endAt = LocalDate.of(2023, 4, 12)))
)

findById()를 직접 스텁(stub)하니 테스트가 정상적으로 동작하는걸 확인할 수 있었다.

 

다만, 실제 코드에서는 findByIdOrNull() 메서드를 사용하는데 findById()를 스텁(Stub)해야 하고
findById()를 사용하면서 코틀린에서 Optional을 사용해야 하는 추가적인 절차가 생겼다.

 

 

확장 함수 모킹


그러면 코틀린의 확장함수는 정말로 모킹이 안되는걸까??

findByIdOrNull()은 내가 직접 만든 확장함수는 아니기 때문에 직접 간단한 확장함수를 만든뒤에 테스트를 해보기로 했다.

 

 

Money.plus() 확장함수 생성

data class Money(
    val price: Int
)

fun Money.plus(price: Int): Money {
    return Money(this.price + price)
}

 

plus() 확장함수 테스트

class ExtensionFunctionTest {
    @Test
    fun `코틀린 확장함수 모킹`() {
        val mock = mock<Money>()
        whenever(mock.plus(100)).thenReturn(Money(1000))  //WrongTypeOfReturnValue 에러 발생

        val result = mock.plus(100)

        println("result.price = ${result.price}")
        assertThat(result.price).isEqualTo(1000)
    }
}

내가 직접 만든 plus()확장함수도 테스트를 돌려보니 동일한 에러가 발생했다.

 

확장함수 스텁(Stub)이 왜 안될까를 알아보았고 그 이유를 코틀린 확장 함수의 특징에서 알 수 있었다.

 

코틀린 확장 함수 특징

  1. 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수이다.
  2. 내부적으로는 클래스 내의 정적 함수(static function)로 실행 된다.
  3. 확장함수를 사용하기 위해서는 클래스와 같이 임포트를 해야한다.

 

3번의 경우 실제로 plus()함수를 사용할려면 임포트를 해야하는걸 코드로 확인할 수 있다.

import com.example.plus
import com.example.Money

val money = Money(100).plus(100)

 

1번 2번의 특징을 살펴보기 위해 Money의 확장함수를 자바언어로 Decompile해보자.

코틀린 언어를 자바로 Decompile하는 방법은 여기를 참고하면 된다.

public final class ExtensionFunctionTestKt {
   @NotNull
   public static final Money plus(@NotNull Money $this$plus, int price) {
      Intrinsics.checkNotNullParameter($this$plus, "$this$plus");
      return new Money($this$plus.getPrice() + price);
   }
}

public final class Money {
   private final int price;

   public final int getPrice() {
      return this.price;
   }

   public Money(int price) {
      this.price = price;
   }

   public final int component1() {
      return this.price;
   }

   @NotNull
   public final Money copy(int price) {
      return new Money(price);
   }

   // $FF: synthetic method
   public static Money copy$default(Money var0, int var1, int var2, Object var3) {
      if ((var2 & 1) != 0) {
         var1 = var0.price;
      }

      return var0.copy(var1);
   }

   @NotNull
   public String toString() {
      return "Money(price=" + this.price + ")";
   }

   public int hashCode() {
      return Integer.hashCode(this.price);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Money) {
            Money var2 = (Money)var1;
            if (this.price == var2.price) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}
  • plus() 확장함수는 ExtensionFunctionTestkt 클래스로 Money클래스와 밖에 선언(1번내용)되어 있는걸 확인
  • plus() 함수는 public static final Money plus()로 정적 함수로(2번내용) 선언되어 있는걸 확인

mokito의 모킹은 인스턴스(클래스) 단위로 모킹을 하게되는데 plus()라는 확장함수는 Money클래스 범위 밖의 함수이기 때문에 Money를 모킹해도 안되는 것이다.

 

추가로 찾아보니 Mockito-Kotlin Issue 글에 이런 답변이 있는걸 발견했다.

답변 내용은 mockito는 static method를 스텁(stub)할 수 없다는 것이다.

의문을 품었던 코틀린의 확장함수를 스텁할 수 없다는 가정이 사실이었던 것이었다.

 

확장함수도 스텁(Stub)할 수 있는 MockK


코틀린의 확장함수 기능을 사용하는 경우가 꽤 있는데 그러면 앞으로 테스트를 진행할 때 어떤방법 사용 해야하나 걱정이 들었다.

다행히도(역시 내가 겪은 일은 이미 다른 누군가 겪고 해결방법이 나와있다..) 이미 코틀린의 확장함수 스텁을 지원해주는 MockK라이브러리가 존재했다.

 

마지막으로 Mockk 라이브러리를 사용해서 맨처음 findByIdOrNull() 확장함수 에러를 해결해보자.

 

환경 설정

Gradle(Kotlin DSL)

testImplementation("io.mockk:mockk:${mockkVersion}")

 

테스트 코드

class CouponServiceMockkTest {

    private val mockkCouponRepository: CouponRepository = mockk()
    private val couponService = CouponService(mockkCouponRepository)

    @Test
    fun `쿠폰 한건 조회`() {
        mockkStatic("org.springframework.data.repository.CrudRepositoryExtensionsKt")

        every {
            mockkCouponRepository.findByIdOrNull(any())
        } returns Coupon(
            id = 1L,
            type = CouponType.PERCENT,
            amount = 10,
            startAt = LocalDate.of(2023, 4, 12),
            endAt = LocalDate.of(2023, 4, 12)
        )

        val result = couponService.getOne(1L)

        assertThat(result.type).isEqualTo(CouponType.PERCENT)
        assertThat(result.amount).isEqualTo(10)
        assertThat(result.startAt).isEqualTo(LocalDate.of(2023, 4, 12))
        assertThat(result.endAt).isEqualTo(LocalDate.of(2023, 4, 12))
    }
}

Mockk 라이브러리를 이용해 확장함수를 stub하는 방법은 mockkStatic() 함수를 이용해서 확장함수의 경로를 넣어주면 된다.

  • 확장함수의 경로 = 패키지 경로 + 컴파일된 Class명

(Mockk의 문법의 자세한 설명은 여기선 생략한다.)

 

이제 드디어 findByIdOrNull()메서드가 정상적으로 스텁되어서 테스트가 통과되는걸 확인할 수 있었다.

 

 

마무리


기존에는 mockito-kotlin을 이용해 주로 테스트를 진행했었는데 

이번 기회에 코틀린을 지원해주는 다른 테스트 라이브러리로 전환을 고려해야겠다는 생각이 들었다.

대표적인 라이브러리로는 Kotest, Mockk가 있는것 같은데 혹시 두개의 라이브러리를 사용해보신 경험이 있다면 댓글로 추천 부탁드립니다!!

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함