🔖 Kotlin

👾 MockK와 Mockito

beomsic 2025. 1. 9. 17:16

📌 Mockito

@Test
fun `mockito 테스트 예제`() {
    // given
    val username = "beomsic"
    val user = User(username)

    given(userRepository.save(any())).willReturn(user)

    // when
    userService.save(user)

    // then
    then(userRepository).should().save(any())
}

Mockito를 사용하면 BDDMockito를 통해서 BDD 형태로 작성할 수 있다.

  • when은 given으로 verify는 then으로 사용

 

📌 MockK

코틀린에서 테스트 시 Mock 객체를 생성하는 것을 도와주는 라이브러리

@Test
fun `mockk 테스트 예제`() {
    // given
    val username = "beomsic"
    val user = User(username)

    every { userRepository.save(any()) } returns (user)

    // when
    userService.save(user)

    // then
    verify { userRepository.save(any()) }
}

 

⚠️ Mockito의 문제점

1️⃣ 확장함수

코틀린에서는 확장 함수 기능을 제공을 해주고 있다.

예외 처리 로직을 확장 함수로 정의하여 중복 코드를 제거하고, 더 간결한 코드로 예외를 처리할 수 있도록 한 예시코드

interface TestRepository : JpaRepository<TestEntity, Long> {
}

fun TestRepository.findEntityById(id: Long): TestEntity {
    return findByIdOrNull(id) ?: throw NotFoundException()
}

----
val entity = testRepository.findEntityById(id)
  • kotlin의 확장 함수를 사용해 새로운 기능을 추가

 

하지만 확장 함수를 사용하면 Mocktio를 사용해 테스트를 할 수 없다.

 

[Mockito-kotlin Issue] https://github.com/mockito/mockito-kotlin/issues/198

  • Mockito으로 Kotlin Extension Function을 Mocking하지 말아라

 

확장 함수는 정적(static) 메서드이다.

  • kotiln의 확장 함수는 실제로 수신 객체의 메서드가 아닌 정적 메서드로 컴파일

 

Mockito 라이브러리는 정적 메서드를 mocking하는 것을 지원하지 않고 있다.

이와 달리, MockK는 확장함수도 mocking 처리가 가능해 의도대로 테스트를 할 수 있다.

 

class TestRepositoryExtensionTest {

    private val testRepository: TestRepository = mockk()

    @Test
    fun `findEntityById`() {
        // given
        val testEntity = TestEntity(id = 1L, name = "Test Entity")
        
        every { testRepository.findEntityById(1L) } returns testEntity

        // when
        val result = testService.findById(1L)

        // then
        verify { userRepository.findEntityById(1L) }
        assertThat(result.name).isEqualTo(testEntity.name)
    }
}

 

2️⃣ object

Kotlin의 object는 프로그램 실행 중 한 번만 초기화되고, 항상 동일한 인스턴스를 사용합니다.

object BCryptUtils {
    fun hash(password: String) =
        BCrypt.withDefaults().hashToString(12, password.toCharArray())

    fun verify(password: String, hashedPassword: String) =
        BCrypt.verifyer().verify(password.toCharArray(), hashedPassword).verified
}

 

이렇게 프로젝트 개발시 유틸리티 class나 mapper 형태에서 주로 사용한다.

Mockito는 기본적으로 클래스의 인스턴스를 대상으로 동작

  • 클래스의 인스턴스 메서드인터페이스 메서드를 가로채(mock)도록 설계

 

❗object는 정적 필드로 관리되는 싱글톤, object의 메서드는 정적 메서드처럼 작동하기 때문에 정적 멤버나 메서드를 기본적으로 지원하지 않는 Mockito는 mocking을 할 수 없다.

 

MockK는 kotlin의 특성을 고려해 설계되었기 때문에 object class의 Mocking을 지원한다.

class BCryptUtilsTest {

    @Test
    fun `test BCryptUtils hash method`() {
        mockkObject(BCryptUtils)

        every { BCryptUtils.hash("password123") } returns "mockedHashedPassword"

        val hashedPassword = BCryptUtils.hash("password123")
        assert(hashedPassword == "mockedHashedPassword")
        unmockkObject(BCryptUtils)
    }
}
  • mockkObject() 함수를 이용해 대상 object를 넣어 기본 mocking 방법처럼 적용
  • unmockkObject() 를 이용해 테스트 후 mocking 해제를 할 수 있다.

 

📖 참고자료

MockK

코틀린 테스트에서 mockk를 써야하는 이유

코틀린에서의 내부 가시성과 MockK를 활용한 테스트 전략

Mock singleton objects and static methods | MockK Guidebook