MockK는 모의 객체를 생성하기 위한 Kotlin-first 프레임워크로, Kotlin 생태계를 위해 코루틴, 확장 함수, data class, sealed class와 같은 언어적 특징을 고려하여 특별히 설계되었습니다. Java에서 Kotlin으로 포팅된 Mockito와 달리, MockK는 처음부터 Kotlin 구문을 위해 설계되었으며 final 클래스 작업에 추가 플러그인이 필요하지 않습니다. MockK.io에 따르면, 이 라이브러리는 단위 테스트를 사용하는 Kotlin 프로젝트의 40% 이상에서 사용됩니다.
핵심 사항
MockK는 모의 객체를 생성하기 위한 라이브러리로, Kotlin으로 작성되었으며 그 구문에 최적화되어 있습니다. Mockito와 동일한 문제(종속성에서 테스트 대상 코드를 격리)를 해결하지만, 람다, DSL, reified generics, suspend 함수와 같은 Kotlin特有 구문을 사용합니다.
포팅된 솔루션에 비해 MockK의 주요 장점은 네이티브 Kotlin 지원입니다. Mockito에서 final 클래스를 모킹하려면 opt-in(mockito-inline)이 필요하고, 정적 메서드에는 mockStatic이 필요합니다. MockK는 기본적으로 이를 지원합니다. Kotlin 클래스는 기본적으로 final이며, 이 제한을 우회하는 것이 라이브러리 아키텍처에 내장되어 있기 때문입니다.
버전 1.13.12(2024)는 Kotlin 2.0, K2 컴파일러 및 멀티플랫폼 프로젝트(KMP)를 지원하는 안정적인 릴리스입니다. MockK는 Kotlin/Native 및 Kotlin/JS에서도 작동하므로 Mockito나 EasyMock을 적용할 수 없는 KMP 프로젝트의 유일한 선택입니다.
MockK는 Kotlin의 특성을 고려하여 설계되었으며, 성능을 희생하지 않으면서 간결하고 타입 안전한 API를 제공하기 위해 reified generics, 람다를 사용한 DSL, 인라인 함수와 같은 언어 기능을 활용합니다.
MockK의 메커니즘은 ByteBuddy 라이브러리(Mockito와 마찬가지)를 통한 바이트코드 계측에 기반하지만, 이를 Kotlin 친화적인 DSL로 래핑합니다. when().thenReturn() 체인 대신 MockK는 언어의 자연스러운 확장처럼 보이는 람다 블록 every { }와 coEvery { }를 사용합니다. 내부적으로 MockK는 람다 내의 호출을 가로채고, 리플렉션을 통해 메서드와 인수를 분석하여 기록된 스터빙 규칙과 일치시킵니다.
블록 every { mock.method() } returns value는 “메서드가 호출될 때마다 값을 반환”으로 읽힙니다. 이 선언적 구문은 Kotlin 스타일에 더 가깝고 when()에서 인수 순서에 대한 혼동을 제거합니다. Kotlin의 reified generics 덕분에 클래스를 명시적으로 지정하지 않아도 모의 객체 유형이 자동으로 추론됩니다.
val repository = mockk<UserRepository>()
// Stubbing: findById(1)의 각 호출은 사용자 반환
every { repository.findById(1) } returns User("Alice")
// 호출 및 확인
val result = repository.findById(1)
assertEquals("Alice", result.name)
각 메서드를 명시적으로 구성해야 하는 Mockito와 달리, MockK는 relaxed mock을 지원합니다. 이는 모든 메서드에 대해 “합리적인” 기본값(List의 경우 빈 목록, Int의 경우 0, String의 경우 빈 문자열)을 반환하는 모의 객체입니다. 이렇게 하면 설정 코드의 양이 크게 줄어듭니다.
// Relaxed mock — 모든 메서드가 기본값 반환
val api = mockk<ApiService>(relaxed = true)
// 스터빙 불필요 — 빈 목록 반환
println(api.getUsers()) // []
MockK는 모의 객체를 생성하는 여러 방법을 제공합니다. 엄격한 모의 객체용 mockk<T>()(각 메서드를 명시적으로 구성해야 함), 릴랙스드 모의 객체용 mockk<T>(relaxed = true), 실제 객체에 스파이를 생성하는 spyk(obj)입니다.
| 함수 | 유형 | 스터빙 없는 동작 |
|---|---|---|
| mockk() | 엄격한 모의 객체 | 스터빙되지 않은 메서드 호출 시 예외 발생 |
| mockk(relaxed = true) | 릴랙스드 모의 객체 | 기본값 반환 |
| spyk() | 스파이 | 스터브가 구성되지 않은 경우 실제 메서드 호출 |
| slot() | Argument Captor | 확인을 위해 인수 캡처 |
엄격한 모의 객체와 릴랙스드 모의 객체 사이의 선택은 컨텍스트에 따라 다릅니다. 엄격한 모의 객체는 테스트가 동작이 정의되지 않은 메서드를 사용하지 않도록 보장하여 신뢰성을 높입니다. 릴랙스드 모의 객체는 모든 종속성이 중요하지 않은 빠른 테스트 프로토타이핑에 편리합니다. 실제로는 엄격한 모의 객체로 시작하고 스터빙이 테스트 자체보다 더 많은 줄을 차지할 때만 릴랙스드로 전환하는 것이 좋습니다.
Every 블록은 MockK의 핵심 스터빙 구문입니다. 람다 내에서 특정 인수를 가진 메서드 호출을 설명하고, returns를 통해 값을 반환하거나, throws를 통해 예외를 발생시키거나, answers를 통해 응답을 계산합니다.
MockK는 테스트에 필요한 모든 시나리오를 지원합니다: 값 반환, 예외 발생, 인수 기반 응답 계산, 순서대로 여러 응답(호출 시퀀스)입니다.
// 값 반환
every { repo.findById(1) } returns User("Alice")
// 예외 발생
every { repo.findById(999) } throws NotFoundException()
// 동적 응답
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// 응답 시퀀스
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify는 MockK에서 목적상 Mockito.verify()와 유사하지만 Kotlin DSL을 사용합니다: verify { mock.method() }. Suspend 함수의 경우 coVerify { mock.suspendMethod() }를 사용하며, 코루틴과 올바르게 작동하고 특별한 실행기가 필요하지 않습니다.
MockK는 Mockito와 동일한 수정자를 지원합니다: exactly(1), atLeast(2), atMost(5), wasNot(Called). 구문은 미니멀하며, 수정자는 verify { }의 첫 번째 인수로 전달됩니다.
// 확인: 메서드가 정확히 1회 호출됨
verify(exactly = 1) { repo.save(any()) }
// 호출 순서 확인
verifySequence {
repo.save(any())
repo.flush()
}
// suspend 함수용 coVerify
coVerify { api.fetchUsers() }
인수 확인을 위해 slot()을 사용합니다. 이는 ArgumentCaptor와 유사합니다. Slot은 호출 전에 선언되고 every나 verify에 전달되며, 테스트 실행 후 캡처된 값을 포함합니다.
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK는 JUnit 5에서 초기화를 통해 모의 객체를 생성하기 위한 @MockK 및 @RelaxedMockK 어노테이션을 제공합니다. MockKExtension은 각 테스트 전에 자동으로 모의 객체를 생성하고 정리합니다. 이는 MockitoExtension과 유사하지만 릴랙스드 모드를 지원합니다.
@InjectMockKs 어노테이션(또는 명시적 객체 생성을 사용하는 대안 @MockK)은 테스트 대상 인스턴스에 모의 객체를 주입합니다. 이는 보일러플레이트를 줄이고 테스트 코드를 더 깔끔하게 만듭니다.
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
lateinit var repository: UserRepository
@InjectMockKs
lateinit var service: UserService
@Test
fun `getUser returns user from repository`() {
every { repository.findById(1) } returns User("Alice")
assertEquals("Alice", service.getUser(1)?.name)
}
}
선택은 팀 구성과 프로젝트 유형에 따라 달라집니다. Mockito는 더 큰 생태계, 더 많은 예제 및 통합을 가지고 있지만, MockK는 더 깔끔한 Kotlin 구문과 언어 기능에 대한 네이티브 지원을 제공합니다. 새로운 Kotlin 프로젝트의 경우 MockK가 더 관용적인 솔루션으로 권장됩니다.
| 기준 | MockK | Mockito |
|---|---|---|
| 구문 | Kotlin DSL (every, verify) | Java 스타일 (when, thenReturn) |
| 코루틴 | coEvery, coVerify (네이티브) | 추가 라이브러리 필요 |
| Final 클래스 | 기본적으로 지원 | mockito-inline 필요 |
| KMP | 지원 | 미지원 |
| Relaxed mock | 내장 | 대응 기능 없음 |
| 인기도 | Kotlin 커뮤니티에서 증가 중 | Java 및 하이브리드 프로젝트에서 지배적 |
순수 Kotlin 프로젝트(Java 클래스 없음)의 경우 MockK가 더 좋습니다: 보일러플레이트 감소, 네이티브 코루틴 지원, final 클래스에 대한 놀라움 없음. 하이브리드 프로젝트 또는 Java 배경을 가진 팀의 경우 Mockito가 여전히 작동 가능한 옵션입니다. 두 라이브러리는 다른 모듈을 통해 동일한 프로젝트에서 사용할 수 있습니다. Mockito에서 MockK로 마이그레이션할 때는 @Mock 어노테이션을 @MockK로 바꾸고 when().thenReturn() 블록을 every { } 형식으로 다시 작성하면 됩니다.
자주 묻는 질문
Relaxed mock은 스터빙되지 않은 모든 메서드에 대해 예외를 발생시키지 않고 기본값(빈 목록, 0, null)을 반환합니다. 일반(엄격한) 모의 객체는 각 메서드의 명시적 스터빙이 필요하며, 그렇지 않으면 테스트가 실패합니다. 릴랙스드 모의 객체는 빠른 테스트에 편리하고, 엄격한 것은 신뢰할 수 있는 테스트에 적합합니다.
MockK는 mockkStatic()을 통한 확장 함수 모킹을 지원합니다. Kotlin의 확장 함수는 수신자를 첫 번째 매개변수로 하는 정적 메서드이기 때문에 가능합니다. 각 확장 함수에 대해, 그것이 선언된 클래스를 지정해야 합니다.
네, MockK는 공통 코드를 위한 Kotlin Multiplatform(KMP)을 지원합니다. JVM, Native 및 JS 플랫폼에서 공통 API(mockk(), every, verify)를 사용할 수 있습니다. 이는 Mockito가 작동하지 않는 KMP 프로젝트의 유일한 선택입니다.
verifySequence { }를 사용하세요. 호출이 예상 순서대로 엄격하게 지정되는 블록입니다. 실제 순서가 다를 경우 verifySequence는 첫 번째 불일치 호출을 나타내는 예외를 발생시킵니다.
네, 기술적으로 가능하지만 권장되지 않습니다. 바이트코드 계측 수준(ByteBuddy vs mockito-inline)에서 충돌이 발생할 수 있습니다. 프로젝트가 이미 Mockito를 사용하는 경우, 모듈 격리를 통해 MockK로 점진적 마이그레이션이 가능합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.