Test Doubles — 대체 객체의 유형과 활용

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

Test Doubles는 실제 의존성 대신 단위 테스트에서 사용되는 대체 객체입니다. 이 용어는 Gerard Meszaros가 저서 “xUnit Test Patterns”(2007)에서 Mock, Stub, Fake, Spy, Dummy의 포괄적 개념으로 도입했습니다. Martin Fowler(2024)에 따르면, Test Doubles는 테스트 대상 컴포넌트를 환경으로부터 격리하여 테스트를 결정적이고 빠르며 외부 서비스에 독립적으로 만듭니다.

핵심 포인트

  • Test Doubles — 테스트에서 모든 유형의 대체 객체를 가리키는 일반 용어
  • Mock은 상호작용을 검증: 어떤 메서드가 어떤 인수로 호출되었는지 확인
  • Stub은 호출 검증 없이 미리 정의된 값을 반환
  • Fake — 단순화된 작동 구현(예: 인메모리 데이터베이스)
  • Spy는 나중에 검증하기 위해 호출을 기록, Dummy는 매개변수를 채움

Test Doubles란?

Test Doubles는 자동차 산업(스턴트 더블)에서 소프트웨어 개발로 가져온 용어입니다. 스턴트 더블이 위험한 장면에서 배우를 대체하듯이, Test Double은 테스트 시나리오에서 실제 컴포넌트를 대체합니다. 이는 실제 의존성을 사용할 수 없거나, 느리거나, 비결정적이거나, 부작용이 있을 때 필요합니다.

Test Double 개념은 다섯 가지 특정 유형을 포함하며, 각각 고유한 작업을 해결합니다. Meszaros의 유형론은 표준이며 모든 현대 테스트 가이드에서 사용됩니다. 유형 간의 차이는 제어 및 검증의 정도에 있습니다: 단순한 매개변수 채움(Dummy)부터 완전한 호출 시퀀스 검증(Mock)까지입니다.

Test Doubles가 필요한 이유

Test Doubles의 주요 목적은 테스트 대상 모듈의 격리입니다. 모바일 개발에서 실제 의존성에는 API 서버, 데이터베이스, 파일 시스템, 디바이스 센서, 시스템 서비스(LocationManager, Camera, Bluetooth)가 포함됩니다. 이러한 컴포넌트를 직접 사용하면 테스트가 느리고 깨지기 쉬우며 환경에 의존하게 됩니다. Google Testing Blog(2023)에 따르면, 잘 격리된 단위 테스트는 밀리초 단위로 실행되는 반면, 통합 테스트는 초 및 분 단위로 실행됩니다.

Test Doubles의 다섯 가지 유형

Gerard Meszaros의 분류는 다섯 가지 유형의 Test Doubles를 포함하며, 행동과 목적이 다릅니다. 이들 간의 차이를 이해하는 것이 적절한 단위 테스트의 기초입니다.

Dummy

Dummy는 테스트 대상 메서드에 전달되지만 절대 사용되지 않는 객체입니다. Dummy는 메서드 시그니처를 충족시키기 위해서만 필요합니다. Kotlin에서는 주로 null, emptyList() 또는 스텁이 있는 객체입니다. Dummy는 로직을 포함해서는 안 됩니다 — 호출되면 테스트가 실패해야 합니다.

Fake

Fake는 인터페이스의 단순화되었지만 작동하는 구현입니다. Mock 및 Stub과 달리 Fake는 실제 비즈니스 로직을 포함하지만 단순화된 형태입니다. 전형적인 예는 데이터베이스 대신 HashMap에 데이터를 저장하는 InMemoryUserRepository입니다. Fake는 실제 인프라의 오버헤드 없이 상태에 의존하는 로직을 테스트해야 할 때 사용됩니다.

유형목적예제
Dummy매개변수 채우기null, 빈 객체
Fake작동하는 단순화된 구현InMemoryRepository
Stub고정된 값 반환when(api.getUser()).thenReturn(user)
Spy검증을 위한 호출 기록verify(spy).save(user)
Mock상호작용 검증verify(mock).sendEmail(email)

Stub

Stub은 특정 호출에 대해 미리 정의된 값을 반환합니다. Stub은 호출되었는지 확인하지 않습니다 — 단순히 데이터를 제공합니다. Mockito에서 Stub은 when(method).thenReturn(value)를 통해 생성됩니다. Stub은 의존성이 특정 값을 반환해야 하지만 호출 자체는 중요하지 않은 테스트에 이상적입니다.

Spy

Spy는 실제 객체를 감싸는 래퍼로, 나중에 검증하기 위해 모든 호출을 기록합니다. Mock과 달리 Spy는 실제 객체에 호출을 위임하지만 호출이 발생했는지 확인할 수 있습니다. Mockito에서 Spy는 spy(realObject)를 통해 생성됩니다. Spy는 실제 객체를 사용하면서 일부 호출을 검증하려는 부분 목킹에 유용합니다.

Mock

Mock은 미리 정의된 호출 기대치를 가진 객체입니다. Mock은 특정 메서드가 특정 인수와 특정 순서로 호출되었는지 검증합니다. Stub과 달리 Mock은 데이터 반환보다 행동 검증에 초점을 맞춥니다. Mock은 모바일 개발에서 가장 강력하고 가장 자주 사용되는 Test Double 유형입니다.

Mock vs Stub: 주요 차이점

MockStub의 차이는 숙련된 개발자들 사이에서도 종종 혼란을 야기합니다. 주요 차이는 목적에 있습니다: Stub은 상태 검증(state verification), Mock은 행동 검증(behavior verification)입니다.

Stub은 다음 질문에 답합니다: “코드가 올바른 결과를 반환했는가?”. Mock은 다음 질문에 답합니다: “코드가 올바른 인수로 올바른 메서드를 호출했는가?”. 모바일 개발에서 Stub은 결과가 중요한 경우(예: 리포지토리의 데이터) 사용되며, Mock은 부작용이 중요한 경우(예: 이메일 전송, 데이터베이스 쓰기) 사용됩니다.

kotlin
// Stub: 상태 검증
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)

// Mock: 행동 검증
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }

Kotlin의 Test Doubles 예제

MockK를 사용한 Kotlin의 다섯 가지 유형의 Test Doubles 실제 예제 — Android 프로젝트에서 가장 인기 있는 목킹 라이브러리입니다.

Fake: InMemoryUserRepository

kotlin
class InMemoryUserRepository : UserRepository {
    private val store = mutableMapOf<String, User>()

    override fun save(user: User) {
        store[user.email] = user
    }

    override fun findByEmail(email: String): User? {
        return store[email]
    }
}

Stub + Mock: UseCase 테스트

kotlin
class RegisterUseCaseTest {
    private val api = mockk<AuthApi>()
    private val repo = spyk(InMemoryUserRepository())
    private val useCase = RegisterUseCase(api, repo)

    fun `register user successfully`() = runTest {
        // Stub: 고정 API 응답 반환
        coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")

        val result = useCase.execute("test@test.com")

        // Verify: 사용자가 저장되었는지 확인
        verify { repo.save(any()) }
        assertTrue(result.isSuccess())
    }
}

Dummy: 사용되지 않는 매개변수로 테스트

kotlin
data class Logger(val appContext: Context, val format: FormatType)

fun `test logger with dummy context`() {
    // Dummy: Context가 Logger 내부에서 사용되지 않음
    val dummyContext = mockk<Context>()
    val logger = Logger(dummyContext, FormatType.JSON)
    assertEquals(FormatType.JSON, logger.format)
}

모바일 개발에서 각 유형 사용 시기

Test Double 유형의 선택은 무엇을 테스트하는지에 따라 달라집니다: 상태, 행동 또는 통합. Android 및 iOS 모바일 개발에서는 다음과 같은 권장 사항이 확립되었습니다.

ViewModel 및 UseCase의 경우

ViewModel을 테스트할 때는 부작용을 생성하는 의존성(리포지토리, 분석, 네비게이션)에는 Mock을 사용하고, 데이터를 반환하는 의존성(API 클라이언트, ContentProvider)에는 Stub을 사용합니다. 이를 통해 ViewModel이 성공 및 오류 시나리오를 모두 올바르게 처리하는지 검증할 수 있습니다.

Repository 및 데이터 레이어의 경우

Repository 수준에서는 Fake(인메모리 데이터베이스 구현)와 Stub(고정 API 응답)을 선호합니다. Fake를 사용하면 SQLite를 설정하지 않고 캐싱 로직과 오프라인 모드를 테스트할 수 있습니다. Stub은 다양한 HTTP 상태(200, 404, 500, timeout)를 시뮬레이션합니다.

  • 비즈니스 로직 단위 테스트 — 모든 외부 의존성에 Mock, 사용되지 않는 매개변수에 Dummy
  • 통합 테스트 — Mock 대신 Fake(컴포넌트가 함께 작동하는지 확인)
  • UI 테스트 — API 응답에 Stub(MockWebServer 또는 WireMock 통해)
  • 캐싱 테스트 — 데이터베이스에 Fake(Room/SQLite 대신 인메모리)
  • 비동기 테스트 — 코루틴 지원 Mock(Flow용 MockK + Turbine)

대체 객체 사용 시 흔한 실수

Test Doubles의 잘못된 사용은 리팩토링할 때마다 깨지는 취약한 테스트의 가장 흔한 원인 중 하나입니다.

과도한 모킹: Mock의 과도한 사용

가장 흔한 실수는 모든 것을 모킹하는 것입니다. 테스트의 모든 의존성이 Mock으로 대체되면 테스트는 실제 행동 검증을 중단합니다. Mock은 외부 의존성에만 사용해야 합니다(네트워크, 데이터베이스, 파일 시스템, 시스템 서비스). 내부 애플리케이션 컴포넌트(Value Object, data class, 단순 유틸리티)는 대체해서는 안 됩니다.

사양 부족: 불충분한 명세

두 번째 실수는 기대치를 정의하지 않고 Mock을 만드는 것입니다. every / when 없이 메서드가 호출되면 Mock은 기본값(null, 0, false)을 반환합니다. 이는 Mock이 조용히 null을 반환하고 테스트가 이를 올바른 행동으로 해석하는 위양성 테스트로 이어질 수 있습니다.

과도한 검증: verify의 과도한 사용

세 번째 실수는 모든 Mock의 모든 호출을 검증하는 것입니다. Verify는 비즈니스 로직 관점에서 중요하게 중요한 호출에만 사용해야 합니다. 과도한 검증은 테스트를 취약하게 만듭니다: 프로덕션 코드의 호출 순서 변경이 행동을 변경하지 않고 테스트를 깨뜨립니다.

자주 묻는 질문

Mock과 Stub의 차이는 무엇인가요?

Stub은 데이터를 반환하고 상태(무엇이 반환되었는지)를 검증하는 반면, Mock은 행동(어떤 메서드가 호출되었는지)을 검증합니다. Stub = “X를 반환해”, Mock = “Y가 인수 Z로 호출되었는지 확인해”. 실제 테스트에서는 하나의 객체가 종종 Stub과 Mock으로 동시에 작동합니다.

Mock 대신 Fake는 언제 사용하나요?

Fake는 상태에 의존하는 로직(캐싱, 오프라인 모드, 트랜잭션)을 테스트할 때 Mock보다 선호됩니다. Fake(인메모리 구현)는 취약한 verify 호출 없이 이러한 시나리오를 테스트할 수 있습니다. Mock은 데이터 전송(분석, 푸시, 이메일) 확인에 더 적합합니다.

Android에 가장 적합한 Test Doubles 라이브러리는 무엇인가요?

Kotlin Android 프로젝트에는 MockK가 권장됩니다. 추가 설정 없이 코루틴, 서스펜드 함수, sealed class, 확장 함수를 지원합니다. Java 프로젝트의 경우 Mockito가 표준으로 남아 있습니다 — 광범위한 문서를 갖춘 가장 인기 있는 라이브러리입니다.

Test Doubles로 Kotlin Flow를 테스트하는 방법은?

Kotlin Flow를 테스트하려면 MockK와 함께 Turbine 라이브러리를 사용하세요. Turbine은 Flow 방출 검증을 단순화합니다: 값의 순서, 스트림 완료 및 예외를 확인할 수 있습니다. Flow용 Stub은 flowOf(value)를 반환하고, Mock은 Flow가 수집되었는지 검증합니다.

UI 테스트에서 Test Doubles를 사용해도 되나요?

네, 하지만 API 응답 수준에서만 가능하며 UI 컴포넌트 수준에서는 안 됩니다. MockWebServer(OkHttp) 및 WireMock 라이브러리는 UI 테스트에서 HTTP 응답을 모킹할 수 있게 해줍니다. UI 컴포넌트 자체(Compose, SwiftUI Views)는 대체해서는 안 됩니다 — 해당 동작은 스크린샷 테스트와 Espresso를 통해 테스트됩니다.

요약

  • Test Doubles — 다섯 가지 유형의 대체 객체를 위한 일반 용어: Mock, Stub, Fake, Spy, Dummy
  • Mock은 행동 검증(verify), Stub은 데이터 반환(thenReturn), Fake는 단순화된 실제 구현으로 작동
  • Spy는 실제 객체를 감싸고 호출을 기록, Dummy는 사용되지 않는 매개변수 채움
  • Gerard Meszaros 유형론은 모든 현대 모킹 프레임워크에서 사용되는 표준 분류입니다
  • Kotlin 프로젝트에는 MockK, Java에는 Mockito, iOS에는 Cuckoo 또는 OHHTTPStubs 권장
  • 흔한 실수: 과도한 모킹(모든 것 대체), 사양 부족(정의되지 않은 기대치), 과도한 검증(verify 남용)
  • Fake는 상태가 있는 로직(캐싱, 오프라인 모드, 트랜잭션) 테스트에서 Mock보다 선호

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

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

프로젝트 논의

더 읽어보기