Mock — 개념, 목 객체 및 테스트 라이브러리

저자: IT Sectr 게시일: 2026-04-10 읽는 시간: 8 분

Mock은 실제 컴포넌트의 동작을 모방하고 이와의 상호작용을 검증할 수 있게 해주는 대체 객체입니다. 단순히 미리 정해진 값을 반환하는 Stub과 달리, Mock은 메서드 호출 사실, 전달된 인수 및 호출 횟수를 기록합니다. Mockito (2024)에 따르면, Mock은 Java 및 Kotlin 프로젝트에서 가장 인기 있는 Test Double 유형이며, 모바일 애플리케이션의 유닛 테스트 70% 이상에서 사용됩니다.

핵심 사항

  • Mock — 상호작용을 검증하는 객체: 어떤 메서드가, 어떤 인수로, 몇 번 호출되었는지 확인
  • Mockito — Java 및 Android 프로젝트에서 Mock을 생성하는 가장 인기 있는 라이브러리
  • MockK — 코루틴과 sealed class를 네이티브 지원하는 Kotlin용 Mockito 대안
  • Behavior verification — Mock과 Stub의 주요 차이점: Mock은 상태가 아닌 동작을 검증
  • Over-mocking — 주요 안티 패턴: mock은 외부 의존성에만 사용해야 함

Mock이란?

Mock은 모킹 프레임워크(Mockito, MockK, EasyMock)에 의해 생성되는 객체로, 인터페이스나 클래스를 시뮬레이션하고 해당 메서드에 대한 모든 호출을 기록합니다. 개발자는 기대 사항을 설정합니다: 메서드 X가 인수 Y로 호출되고 Z를 반환합니다. 테스트 실행 후, Mock은 기대 사항이 실제 호출과 일치하는지 검증합니다.

이 용어는 Test Doubles의 연극 은유에서 유래했습니다: Mock은 (Dummy처럼) 단순히 무대에 서 있는 것이 아니라 역할을 수행하고 자신과의 상호작용이 올바른지 검증하는 “모방자”입니다. 테스트 대상 코드가 Mock이 기대한 메서드를 호출하지 않았거나 잘못된 인수로 호출한 경우 — 테스트는 기대 위반 메시지와 함께 실패합니다.

Mock 작동 방식

Mock은 프레임워크 팩토리를 통해 생성됩니다: mockk<MyInterface>() 또는 Mockito.mock(MyClass.java). 프레임워크는 모든 메서드 호출을 가로채는 프록시 객체를 생성합니다. 각 호출은 미리 정의된 기대 사항과 비교됩니다. 호출이 기대 사항과 일치하면 지정된 값이 반환되고, 일치하지 않으면 구성에 따라 기본값이 반환되거나 예외가 발생합니다.

Mock이 필요한 경우

Mock은 테스트 대상 코드가 부작용이 있는 컴포넌트(서버로 데이터 전송, 데이터베이스 쓰기, 로깅, 분석, 내비게이션, 시스템 대화상자 표시)와 상호작용할 때 필수적입니다. Mock 없이는 실제 인프라를 실행하지 않고 이러한 상호작용을 검증할 수 없습니다. Google Testing Blog에 따르면, Mock은 테스트 서버를 가동하지 않고 애플리케이션이 실제로 분석 이벤트를 전송했는지 확인하는 유일한 방법입니다.

Mock vs Stub: 상세 비교

MockStub의 차이는 테스트에서 가장 논쟁이 되는 주제 중 하나입니다. 두 유형 모두 실제 의존성을 대체하지만, 근본적으로 다른 방식으로 수행합니다.

기준MockStub
주요 질문메서드가 호출되었는가?어떤 결과가 반환되었는가?
검증동작 (verify)상태 (assert)
데이터 반환선택 사항필수
예제verify(analytics).logEvent(“click”)assertEquals(5, repository.getCount())
사용 시기부작용데이터 반환

실용적 규칙: Mock인가 아닌가

결정을 위한 간단한 테스트: 스스로에게 물어보세요 “이 코드 줄을 삭제하면 테스트가 실패할까?” 테스트가 반환 값을 검증한다면 Stub(assert 기반 검증)이 필요합니다. 테스트가 코드가 올바른 인수로 메서드를 호출했는지 검증한다면 Mock(verify 기반 검증)이 필요합니다. 이 이분법은 Command-Query Separation 패턴을 따릅니다: 상태를 변경하는 메서드(commands)는 Mock이 필요하고, 데이터를 반환하는 메서드(queries)는 Stub이 필요합니다.

Mockito vs MockK: 라이브러리 비교

MockitoMockK 중 선택은 Kotlin으로 Android 프로젝트의 테스트 스택을 설정할 때 첫 번째 결정 중 하나입니다. 두 라이브러리 모두 동일한 목적을 수행하지만, Kotlin 특화 기능에 대한 접근 방식이 다릅니다.

Mockito: 검증된 클래식

Mockito는 Java 프로젝트의 사실상 표준입니다. 버전 5.x는 내장 MockMaker 덕분에 final 클래스, 정적 메서드 및 생성자 모킹을 지원합니다. Kotlin 프로젝트의 경우 Mockito는 추가 설정이 필요합니다: 개선된 구문을 위한 mockito-kotlin 확장, final 클래스를 위한 mockito-inline. Mockito는 추가 어댑터 없이 Kotlin 코루틴 및 suspend 함수를 지원하지 않습니다.

MockK: Kotlin 우선 접근 방식

MockK는 Kotlin을 위해 특별히 만들어졌습니다. 코루틴(coEvery, coVerify), sealed class, data class, 객체 싱글톤 및 확장 함수를 네이티브 지원합니다. MockK 구문은 람다 블록을 사용한 DSL을 사용하며, Kotlin 코드에서 자연스럽게 느껴집니다. MockK는 추가 설정 없이 프로퍼티 모킹도 지원하며, 이는 LiveData, StateFlow 및 Delegates를 사용하는 Android 프로젝트에 중요합니다.

kotlin
// 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초를 절약합니다.

Kotlin Mock 테스트 예제

세 가지 시나리오를 살펴보겠습니다: Mock 의존성을 사용한 ViewModel 테스트, API 호출 검증을 통한 UseCase 테스트, coVerify를 사용한 코루틴 테스트입니다.

예제 1: Mock 분석과 함께하는 ViewModel

kotlin
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") }
    }
}

예제 2: 비동기 검증을 통한 UseCase

kotlin
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)
    }
}

예제 3: ArgumentCaptor를 사용한 인수 검증

kotlin
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의 효과적인 사용은 규율을 필요로 합니다. 이러한 규칙을 위반하면 테스트는 리팩토링할 때마다 깨지는 취약한 장애물로 변합니다.

애플리케이션 외부 경계만 Mock 처리

엄격한 규칙: Mock은 애플리케이션 경계를 넘는 의존성에 대해서만 생성해야 합니다: API 클라이언트, 데이터베이스, 파일 시스템, 시스템 서비스(LocationManager, BluetoothAdapter, Camera). 내부 애플리케이션 클래스 — 도메인 엔터티, Value Objects, 단순 유틸리티 — 는 Mock으로 대체해서는 안 됩니다. 해당 동작은 실제 객체를 통해 테스트됩니다.

테스트당 하나의 assert / verify

각 테스트는 정확히 하나의 논리적 검사 — verify(Mock의 경우) 또는 assert(Stub의 경우) — 를 포함해야 합니다. 하나의 테스트에서 상태와 동작 검증을 혼합하지 마세요. API 호출과 그 결과를 모두 확인해야 하는 경우 — 별도의 이름으로 두 개의 개별 테스트를 만드세요. “테스트당 하나의 assert”로 알려진 이 규칙은 Kent Beck(2002)의 권장 사항에서 비롯되었습니다.

  • Unit을 반환하는 메서드에는 MockK에서 relaxUnitFun = true 사용 — 그렇지 않으면 Mock이 지정되지 않은 호출에서 예외를 발생시킵니다
  • verify를 중요한 호출로만 제한 — 모든 getter와 setter를 검증하지 마세요, 테스트가 취약해집니다
  • ArgumentMatchers를 신중하게 적용 — 인수가 비즈니스 로직에 중요한 경우 any()는 중요한 세부 정보를 숨깁니다
  • verifyNoMoreInteractions를 과도하게 사용하지 마세요 — 이 메서드는 프로덕션 코드의 변경에 대해 테스트를 불필요하게 경직되게 만듭니다
  • @MockkAnnotations 사용하여 Mock 객체 자동 초기화 — 보일러플레이트를 줄이고 가독성을 향상시킵니다

고급 Mock 테스트 기술

기본 모킹 외에도 모바일 개발에서 특정 작업을 해결하는 고급 기술이 있습니다: 멀티스레딩 테스트, Flow 상태 검증 및 실제 객체의 부분 모킹입니다.

spyK를 사용한 부분 Mock

Spy(또는 부분 mock)는 실제 구현에 호출을 위임하면서 개별 메서드를 재정의할 수 있는 객체를 생성할 수 있게 합니다. MockK에서 spyk는 실제 클래스 인스턴스를 기반으로 생성됩니다: val repo = spyk(InMemoryUserRepository()). every를 통해 정의된 기대 사항이 있는 호출은 Mock을 통해 전달되고, 나머지는 실제 객체를 통해 전달됩니다. Spy는 의존성 주입이 아직 구현되지 않은 레거시 코드를 테스트하고 하나의 메서드만 재정의해야 하는 경우에 특히 유용합니다.

Turbine을 사용한 StateFlow 테스트

Jetpack Compose를 사용하는 최신 Android 프로젝트에서 ViewModel은 StateFlow를 통해 상태를 노출합니다. MockK는 Flow 의존성 모킹을 허용하고, Turbine 라이브러리는 방출 검증을 단순화합니다. 클래식 패턴: Flow를 반환하는 UseCase에는 MockK, ViewModel 방출 검증에는 Turbine. 이 스택은 Kotlin Coroutines를 사용하는 프로젝트에 대해 Android Testing 문서(Google, 2024)에서 권장됩니다.

kotlin
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과 Mockito의 차이점은 무엇인가요?

Mock은 개념으로, 동작을 검증하는 Test Double의 한 유형입니다. Mockito는 Java 및 Android에서 Mock 객체를 생성하기 위한 라이브러리입니다. 다른 라이브러리: MockK(Kotlin), EasyMock(Java), Cuckoo(iOS).

Mock은 Kotlin 코루틴과 어떻게 작동하나요?

Mock으로 suspend 함수를 테스트하려면 MockK(coEvery / coVerify) 또는 mockito-kotlin과 함께 Mockito를 사용하세요. MockK는 코루틴을 네이티브 지원합니다: coEvery는 suspend 함수의 동작을 정의하고, coVerify는 코루틴 내에서의 호출을 검증합니다. 모든 suspend 호출은 runTest(kotlinx-coroutines-test) 내에서 실행되어야 합니다.

Mock은 반복 호출 시 다른 값을 반환할 수 있나요?

네. MockK에서는 returnsMany를 사용합니다: every { api.getData() } returnsMany listOf(response1, response2). Mockito에서는 thenReturn(value1).thenReturn(value2) 체인을 사용합니다. 이는 다른 응답을 반환하는 순차적 호출로 동작을 테스트하는 데 유용합니다.

테스트 간 Mock 상태를 지우려면 어떻게 하나요?

MockK에서는 relaxed = true@MockK 어노테이션을 사용하고 @After 메서드에서 clearMocks(mock)을 호출합니다. Mockito에서는 Mockito.reset(mock)을 사용합니다. 모범 사례: @Before를 통해 각 테스트마다 새 Mock을 생성하여 테스트 간 간섭을 제거합니다.

Mock은 Kotlin에서 sealed class를 어떻게 처리하나요?

MockK는 sealed class에서 올바르게 작동합니다: every { useCase() } returns Result.Success(data). Mockito는 sealed class를 직접 지원하지 않으며 해결 방법이 필요합니다. 이것이 Kotlin 프로젝트에서 Mockito보다 MockK가 권장되는 이유 중 하나입니다.

요약

  • Mock — 의존성의 상태(assert)가 아닌 동작(verify)을 검증하는 Test Double 유형
  • Mockito — Java/Android 표준, MockK — 코루틴 및 sealed class를 지원하는 Kotlin 우선 선택
  • 기본 규칙: 외부 경계(네트워크, DB, 시스템 서비스)에는 Mock, 내부 클래스에는 실제 객체
  • Over-mocking — 주요 안티 패턴: 과도한 의존성 대체는 테스트를 취약하고 쓸모없게 만듦
  • 하나의 테스트 — 하나의 논리적 검사: Mock의 경우 verify, Stub의 경우 assert, 동일 테스트에서 둘 다 사용 금지
  • ArgumentCaptor / slot — 맹목적인 any() 대신 Mock 호출 인수를 검증하는 올바른 방법
  • MockK는 Kotlin 프로젝트에 권장: coEverycoVerify는 추가 어댑터 없이 코루틴과 네이티브로 작동

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기