Mock은 실제 컴포넌트의 동작을 모방하고 이와의 상호작용을 검증할 수 있게 해주는 대체 객체입니다. 단순히 미리 정해진 값을 반환하는 Stub과 달리, Mock은 메서드 호출 사실, 전달된 인수 및 호출 횟수를 기록합니다. Mockito (2024)에 따르면, Mock은 Java 및 Kotlin 프로젝트에서 가장 인기 있는 Test Double 유형이며, 모바일 애플리케이션의 유닛 테스트 70% 이상에서 사용됩니다.
핵심 사항
Mock은 모킹 프레임워크(Mockito, MockK, EasyMock)에 의해 생성되는 객체로, 인터페이스나 클래스를 시뮬레이션하고 해당 메서드에 대한 모든 호출을 기록합니다. 개발자는 기대 사항을 설정합니다: 메서드 X가 인수 Y로 호출되고 Z를 반환합니다. 테스트 실행 후, Mock은 기대 사항이 실제 호출과 일치하는지 검증합니다.
이 용어는 Test Doubles의 연극 은유에서 유래했습니다: Mock은 (Dummy처럼) 단순히 무대에 서 있는 것이 아니라 역할을 수행하고 자신과의 상호작용이 올바른지 검증하는 “모방자”입니다. 테스트 대상 코드가 Mock이 기대한 메서드를 호출하지 않았거나 잘못된 인수로 호출한 경우 — 테스트는 기대 위반 메시지와 함께 실패합니다.
Mock은 프레임워크 팩토리를 통해 생성됩니다: mockk<MyInterface>() 또는 Mockito.mock(MyClass.java). 프레임워크는 모든 메서드 호출을 가로채는 프록시 객체를 생성합니다. 각 호출은 미리 정의된 기대 사항과 비교됩니다. 호출이 기대 사항과 일치하면 지정된 값이 반환되고, 일치하지 않으면 구성에 따라 기본값이 반환되거나 예외가 발생합니다.
Mock은 테스트 대상 코드가 부작용이 있는 컴포넌트(서버로 데이터 전송, 데이터베이스 쓰기, 로깅, 분석, 내비게이션, 시스템 대화상자 표시)와 상호작용할 때 필수적입니다. Mock 없이는 실제 인프라를 실행하지 않고 이러한 상호작용을 검증할 수 없습니다. Google Testing Blog에 따르면, Mock은 테스트 서버를 가동하지 않고 애플리케이션이 실제로 분석 이벤트를 전송했는지 확인하는 유일한 방법입니다.
Mock과 Stub의 차이는 테스트에서 가장 논쟁이 되는 주제 중 하나입니다. 두 유형 모두 실제 의존성을 대체하지만, 근본적으로 다른 방식으로 수행합니다.
| 기준 | Mock | Stub |
|---|---|---|
| 주요 질문 | 메서드가 호출되었는가? | 어떤 결과가 반환되었는가? |
| 검증 | 동작 (verify) | 상태 (assert) |
| 데이터 반환 | 선택 사항 | 필수 |
| 예제 | verify(analytics).logEvent(“click”) | assertEquals(5, repository.getCount()) |
| 사용 시기 | 부작용 | 데이터 반환 |
결정을 위한 간단한 테스트: 스스로에게 물어보세요 “이 코드 줄을 삭제하면 테스트가 실패할까?” 테스트가 반환 값을 검증한다면 Stub(assert 기반 검증)이 필요합니다. 테스트가 코드가 올바른 인수로 메서드를 호출했는지 검증한다면 Mock(verify 기반 검증)이 필요합니다. 이 이분법은 Command-Query Separation 패턴을 따릅니다: 상태를 변경하는 메서드(commands)는 Mock이 필요하고, 데이터를 반환하는 메서드(queries)는 Stub이 필요합니다.
Mockito와 MockK 중 선택은 Kotlin으로 Android 프로젝트의 테스트 스택을 설정할 때 첫 번째 결정 중 하나입니다. 두 라이브러리 모두 동일한 목적을 수행하지만, Kotlin 특화 기능에 대한 접근 방식이 다릅니다.
Mockito는 Java 프로젝트의 사실상 표준입니다. 버전 5.x는 내장 MockMaker 덕분에 final 클래스, 정적 메서드 및 생성자 모킹을 지원합니다. Kotlin 프로젝트의 경우 Mockito는 추가 설정이 필요합니다: 개선된 구문을 위한 mockito-kotlin 확장, final 클래스를 위한 mockito-inline. Mockito는 추가 어댑터 없이 Kotlin 코루틴 및 suspend 함수를 지원하지 않습니다.
MockK는 Kotlin을 위해 특별히 만들어졌습니다. 코루틴(coEvery, coVerify), sealed class, data class, 객체 싱글톤 및 확장 함수를 네이티브 지원합니다. MockK 구문은 람다 블록을 사용한 DSL을 사용하며, Kotlin 코드에서 자연스럽게 느껴집니다. MockK는 추가 설정 없이 프로퍼티 모킹도 지원하며, 이는 LiveData, StateFlow 및 Delegates를 사용하는 Android 프로젝트에 중요합니다.
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
벤치마크(JVM Benchmark, 2024)에 따르면 MockK는 Java Reflections 대신 Kotlin 바이트코드로 직접 작업하기 때문에 Kotlin 프로젝트에서 Mockito보다 15–20% 더 빠르게 목 객체를 생성합니다. 수천 개의 유닛 테스트가 있는 프로젝트의 경우 빌드 속도 차이가 눈에 띌 수 있습니다: MockK는 대규모 프로젝트의 전체 테스트 실행에서 30–60초를 절약합니다.
세 가지 시나리오를 살펴보겠습니다: Mock 의존성을 사용한 ViewModel 테스트, API 호출 검증을 통한 UseCase 테스트, coVerify를 사용한 코루틴 테스트입니다.
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
모바일 개발에서 Mock의 효과적인 사용은 규율을 필요로 합니다. 이러한 규칙을 위반하면 테스트는 리팩토링할 때마다 깨지는 취약한 장애물로 변합니다.
엄격한 규칙: Mock은 애플리케이션 경계를 넘는 의존성에 대해서만 생성해야 합니다: API 클라이언트, 데이터베이스, 파일 시스템, 시스템 서비스(LocationManager, BluetoothAdapter, Camera). 내부 애플리케이션 클래스 — 도메인 엔터티, Value Objects, 단순 유틸리티 — 는 Mock으로 대체해서는 안 됩니다. 해당 동작은 실제 객체를 통해 테스트됩니다.
각 테스트는 정확히 하나의 논리적 검사 — verify(Mock의 경우) 또는 assert(Stub의 경우) — 를 포함해야 합니다. 하나의 테스트에서 상태와 동작 검증을 혼합하지 마세요. API 호출과 그 결과를 모두 확인해야 하는 경우 — 별도의 이름으로 두 개의 개별 테스트를 만드세요. “테스트당 하나의 assert”로 알려진 이 규칙은 Kent Beck(2002)의 권장 사항에서 비롯되었습니다.
기본 모킹 외에도 모바일 개발에서 특정 작업을 해결하는 고급 기술이 있습니다: 멀티스레딩 테스트, Flow 상태 검증 및 실제 객체의 부분 모킹입니다.
Spy(또는 부분 mock)는 실제 구현에 호출을 위임하면서 개별 메서드를 재정의할 수 있는 객체를 생성할 수 있게 합니다. MockK에서 spyk는 실제 클래스 인스턴스를 기반으로 생성됩니다: val repo = spyk(InMemoryUserRepository()). every를 통해 정의된 기대 사항이 있는 호출은 Mock을 통해 전달되고, 나머지는 실제 객체를 통해 전달됩니다. Spy는 의존성 주입이 아직 구현되지 않은 레거시 코드를 테스트하고 하나의 메서드만 재정의해야 하는 경우에 특히 유용합니다.
Jetpack Compose를 사용하는 최신 Android 프로젝트에서 ViewModel은 StateFlow를 통해 상태를 노출합니다. MockK는 Flow 의존성 모킹을 허용하고, Turbine 라이브러리는 방출 검증을 단순화합니다. 클래식 패턴: Flow를 반환하는 UseCase에는 MockK, ViewModel 방출 검증에는 Turbine. 이 스택은 Kotlin Coroutines를 사용하는 프로젝트에 대해 Android Testing 문서(Google, 2024)에서 권장됩니다.
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
자주 묻는 질문
Mock은 개념으로, 동작을 검증하는 Test Double의 한 유형입니다. Mockito는 Java 및 Android에서 Mock 객체를 생성하기 위한 라이브러리입니다. 다른 라이브러리: MockK(Kotlin), EasyMock(Java), Cuckoo(iOS).
Mock으로 suspend 함수를 테스트하려면 MockK(coEvery / coVerify) 또는 mockito-kotlin과 함께 Mockito를 사용하세요. MockK는 코루틴을 네이티브 지원합니다: coEvery는 suspend 함수의 동작을 정의하고, coVerify는 코루틴 내에서의 호출을 검증합니다. 모든 suspend 호출은 runTest(kotlinx-coroutines-test) 내에서 실행되어야 합니다.
네. MockK에서는 returnsMany를 사용합니다: every { api.getData() } returnsMany listOf(response1, response2). Mockito에서는 thenReturn(value1).thenReturn(value2) 체인을 사용합니다. 이는 다른 응답을 반환하는 순차적 호출로 동작을 테스트하는 데 유용합니다.
MockK에서는 relaxed = true로 @MockK 어노테이션을 사용하고 @After 메서드에서 clearMocks(mock)을 호출합니다. Mockito에서는 Mockito.reset(mock)을 사용합니다. 모범 사례: @Before를 통해 각 테스트마다 새 Mock을 생성하여 테스트 간 간섭을 제거합니다.
MockK는 sealed class에서 올바르게 작동합니다: every { useCase() } returns Result.Success(data). Mockito는 sealed class를 직접 지원하지 않으며 해결 방법이 필요합니다. 이것이 Kotlin 프로젝트에서 Mockito보다 MockK가 권장되는 이유 중 하나입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.