Stub(스탁, 테스트 더블)—실제 구현 대신 메소드 호출에 미리 정의된 응답을 반환하는 테스트 객체입니다. 모바일 개발에서 스탁은 테스트 대상 모듈을 네트워크 요청, 데이터베이스, 파일 시스템으로부터 격리하여 환경 설정 없이 논리를 검증할 수 있게 합니다. Mock과 달리, Stub은 동작을 검증하지 않습니다—단지 데이터를 제공할 뿐입니다. 자세한 내용은 Martin Fowler의 테스트 더블 관련 글을 참조하세요.
주요 검토 사항
Stub은 테스트에서 실제 의존성을 대체하고 특정 호출에 대해 미리 정의된 값을 반환하는 테스트 더블 객체입니다. 이 용어는 Gerard Meszaros(2007)의 분류에서 책 ꬼxUnit Test Patternsꬽ에 도입되었습니다. Stub은 테스트 더블 카테고리에 속합니다—테스트 중에 실제 컴포넌트를 대체하는 객체입니다. 스탁의 주요 목적은 테스트 대상 유닛에 예측 가능한 데이터를 제공하고, 외부 시스템의 불확실성을 제거하는 것입니다.
작동 방식—테스트는 실행 전에 스탁을 구성합니다: “getUsers() 메소드가 호출되면, 이 사용자 목록을 반환해라”. Stub에는 비즈니스 논리가 없고, 호출 순서를 검증하지 않으며, 호출 이력을 기록하지 않습니다. 단지 실제 컴포넌트 자리에 서서 지시된 대로 반환할 뿐입니다. Android 테스팅 문맥에서 이는 OkHttp 클라이언트가 서버에 실제 요청을 보내지 않고, 스탁으로 구성된 MockWebServer로부터 응답을 받는 것을 의미합니다.
사용 시기—스탁은 UI 계층(ViewModel, Presenter)과 비즈니스 논리(UseCase, Interactor)를 테스트하는 데 최적적입니다. 특정 데이터(빈 목록, 서버 500 오류, 토큰 만료)에 대한 반응을 검증해야 할 때입니다. 테스트가 특정 입력 상태를 필요로 하는 이보들은 모두 스탁의 역할입니다. 각 테스트 시나리오는 자체 스탁 구성을 가지뭐, 테스트를 읽기 쉽고 예측 가능하게 만듭니다.
Gerard Meszaros(2007)는 책 ꬼxUnit Test Patternsꬽ에서 다섟 가지 테스트 더블을 제시했습니다: dummy, stub, spy, mock, fake. 각 유형은 각기의 문제를 해결합니다. Dummy—전달되지만 사용되지 않음. Stub—데이터를 반환. Spy—호출을 기록. Mock—동작을 검증. Fake—간소화된 논리 포함. 이 분류를 이해하면 개발자가 각 테스트 시나리오에 맞는 도구를 선택할 수 있습니다.
네트워크 요청—스탁 사용의 가장 일반적인 시나리오입니다. 앱이 API에 HTTP 호출을 보내고, 테스트는 다양한 응답(성공적인 JSON, 401 오류, 시간 초과, 빈 배열)에 대한 반응을 검증해야 합니다. Android의 MockWebServer(OkHttp)와 URLProtocol(iOS)은 실제 서버 연결 없이 미리 정의된 HTTP 응답을 반환하는 스탁으로 작동합니다. 이를 통해 테스트는 초에서 밀리초로 가속됩니다.
데이터베이스—Room(Android)과 CoreData(iOS)는 인메모리 변형이 있지만, 설정에 시간이 걸립니다. 리포지토리 대신 Stub은 데이터베이스를 건드리지 않고 미리 준비된 Entity 목록을 반환합니다. 이것은 정렬, 필터링 또는 데이터 변환을 검증해야 하는 ViewModel 테스트에 특히 효과적입니다. 데이터 양에 관계없이 테스트가 밀리초로 실행됩니다.
시스템 서비스—LocationManager, SensorManager, SharedPreferences는 실제 기기나 에미레이터가 필요합니다. LocationProvider용 Stub은 미리 정의된 좌표를, SensorManager용 Stub은 고정된 가속도 값을 반환합니다. iOS에서는 CLLocationManager와 테스트 데리게이트 구현이 이에 해당합니다. 스탁이 없으면 이러한 테스트는 특정 조건이 있는 물리적 기기가 필요합니다.
파일 시스템과 캐시—이미지 로딩, 응답 캐싱, 구성 파일 처리—이 모든 작업은 디스크 상태에 의존합니다. FileManager 또는 ImageCache용 Stub은 실제 파일을 읽지 않고 성공/오류를 반환합니다. 이를 통해 다른 개발자 머신의 경로 불일치 또는 접근 권한으로 인한 가공의 테스트 실패가 제거됩니다.
역할 분담—세 유형의 테스트 더블은 서로 다른 과제를 해결합니다. Stub: “데이터를 반환해라”. Mock: “내가 호출되었는지 검증해라”. Fake: “실제처럼 동작하지만 더 간단하게”. 이 차이는 테스트 가독성에 결정적입니다. Stub이 필요한 곳에 mock을 사용하면, 테스트가 테스트 시나리오와 무관한 verify 호출로 가득 차게됩니다.
| 특징 | Stub | Mock | Fake |
|---|---|---|---|
| 목적 | 데이터 제공 | 상호작용 검증 | 간소화 구현 |
| 논리 | 없음 | 없음 | 있음(하지만 간소화) |
| 검증 | 없음 | 있음(verify) | 간접적(상태 통해) |
| 유연성 | 낮음—고정 응답 | 중간 | 높음—논리가 적응 |
| 속도 | 최대 | 높음 | 중간 |
| 예 | MockWebServer가 JSON 반환 | Mockito.verify(repository).save() | HashMap을 사용하는 InMemoryRepository |
실무 규칙—테스트가 테스트 대상 컴포넌트가 받은 데이터를 검증하는 경우, Stub을 사용하세요. 테스트가 컴포넌트가 올바른 인수로 의존성 메소드를 호출했는지 검증하는 경우, Mock을 사용하세요. 데이터베이스를 해시 테이블로 대체하고자 한다면—그것은 Fake입니다. 한 테스트에서 유형을 혼용하면 테스트가 괴상해집니다: 구현이 바뀜 때, Stub과 verify 모두의 논리를 재작성해야 합니다.
verify가 있는 Stub—개발자가 Stub을 설정한 후 verify(stub).method()을 추가하는 흔한 실수입니다. Stub은 정의상 검증되어서는 안 됩니다—검증을 위한 Mock이 있습니다. 특정 인수로 메소드가 호출되었는지 검증해야 한다면, Mockito.stub() 대신 Mockito.mock()을 사용하세요. 이 구분은 다른 개발자들에게 테스트의 도사를 명확하게 합니다.
MockWebServer—Android와 JVM에서 HTTP 스탁을 만들기 위한 OkHttp 라이브러리입니다. 지정된 포트에 로컬 HTTP 서버를 실행하여 OkHttp 클라이언트의 요청을 가로채는 다음, 미리 정의된 응답을 반환합니다. 설정은 세 줄로 처리됩니다: 서버 생성, 응답 언큐, 시작. 테스트는 페이징 또는 재다시도 시나리오를 위해 여러 응답을 순차적으로 언큐할 수 있습니다.
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK—코루틴, 확장 함수, 실드 클래스를 첫 번째로 지원하는 Kotlin용 Mockito 대체제입니다. MockK에서 스탁은 coEvery(suspend 함수용)와 every(일반 함수용)를 통해 생성됩니다. MockWebServer와 달리, MockK는 전체 HTTP 계층 대신 개별 의존성 메소드를 스탁합니다. 이것은 의존성이 리포지토리 추상화인 UseCase 또는 Interactor의 단위 테스트에 편리합니다.
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
베스트 프렉티스—통합 테스트에는 MockWebServer(실제 HTTP 가로채기)를, 단위 테스트에는 MockK(인터페이스 스탁)을 사용하세요. 테스트하지 않는 것을 스탁하지 마세요. 테스트가 Repository를 검증하는 경우, 그 내부의 OkHttp 클라이언트는 스탁하지 말고 HTTP 계층에서 실제 MockWebServer를 사용하세요. 이 규칙은 테스트를 적절하게 유지하고 리팩토링 시 괴상성을 줄입니다.
Swift 프로토콜을 스탁으로—iOS 네이티브 접근법에서는 의존성 프로토콜을 따르는 테스트 구조체를 대체 삽입하여 스탁을 구현합니다. 실제 NetworkService 대신 테스트는 고정 데이터를 반환하는 StubNetworkService를 받습니다. Swift는 정적 타입 언어이뭐로, 스탁은 실제 서비스와 동일한 프로토콜을 따르는 것이 요구됩니다. 컴파일러가 스탁이 필요한 모든 메소드를 구현하도록 보장합니다.
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
Objective-C용 OCMock—레거시 iOS 프로젝트에서 스탁과 목을 만드는 라이브러리입니다. OCMock은 인수와 반환 값을 가진 스탁 메소드를 지원합니다. 현대 Swift 프로젝트는 매뉴얼 스탁과 함께 프로토콜 기반 접근법을 선호합니다—이는 각 메소드를 제어할 수 있고 외부 의존성이 필요없습니다. OCMock은 모든 의존성을 프로토콜화하는 것이 경제적으로 비효율적인 프로젝트를 위한 선택지로 남아 있습니다.
HTTP 스탁용 URLProtocol—URLProtocol 클래스를 통해 네트워크 요청을 가로채기하는 iOS의 시스템 메커니즘입니다. 테스트는 URLSession을 가로채운 다음 스탁 응답을 반환하는 커스텀 URLProtocol을 등록합니다. 매뉴얼 스탁에 비한 장점: 앱 아키텍처를 변경할 필요가 없습니다—URLSession은 실제로 남아 있지만 데이터가 프로토콜 레벨에서 대체됩니다. 단점: 명시적인 스탁 서비스보다 디버그하기 어렵습니다.
자주 묻는 질문
Stub은 미리 정의된 데이터를 반환하고 호출이 일어났는지 확인하지 않습니다. Mock은 추가로 메소드가 올바른 인수로 호출되었는지 검증(verify)합니다. Stub은 “무엇을 반환할까”에 답하고, Mock은 “호출이 있었나”에 답합니다. 상태 검증에는 Stub을, 상호작용 검증에는 Mock을 사용하세요.
Fake는 테스트가 작동하는(간소화되었지만) 구현이 필요할 때 사용됩니다—예를 들어 Room 대신 인메모리 데이터베이스. Stub은 미리 정의된 데이터를 가진 단일 시나리오에 적합합니다. 10개의 테스트에서 동일한 Stub을 반복하고 있다면, 아맄 Fake가 필요할 것입니다. Fake는 논리가 한 클래스에 모여 있으뭐로 중복성을 줄입니다.
Android에서—MockK는 Kotlin 객체(object)에 대한 mockkObject()를 지원하고, mockkStatic()을 통해 Java 클래스의 정적 메소드까지 포함합니다. iOS에서—Swift 정적 메소드는 직접 스탁할 수 없습니다. static 호출을 프로토콜의 인스턴스 메소드로 대체하려면 프로토콜과 DI를 사용하세요. 정적 스탁은 기술 부쪽이다. 새 코드에서는 피하시는 것이 좋습니다.
MockWebServer(OkHttp)를 사용하세요—것은 응답을 언큐하는 로컬 HTTP 서버로 동작합니다. Retrofit의 경우 기본 URL을 localhost:8080으로 바꾸면 됩니다. Ktor의 경우 MockEngine을 사용하세요—HttpStatement를 대체하는 내장 메커니즘입니다. 둘 다 실제 인터넷 없이 동작하고 상태 코드, 본문, 응답 헤더를 완전히 제어할 수 있습니다.
Spy는 실제 객체를 래핑하고 호출을 기록하는 반면, Stub은 고정 응답으로 객체를 완전히 대체합니다. Spy는 실제 구현을 일부적으로 사용할 수 있지만(다른 메소드는 그대로 작동), Stub은 그렇지 않습니다. 메소드가 호출되었는지 확인해야 하지만 논리의 일부가 실행되어야 한다면—Stub 대신 Spy를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.