Fake는 실제 컴포넌트처럼 동작하지만 프로덕션 인프라 대신 인메모리 스토리지나 기타 경량 메커니즘을 사용하는 의존성의 단순화된 작동 구현체입니다. Stub과 달리 fake에는 실제 비즈니스 로직(정렬, 필터링, 집계)이 포함되어 있지만 외부 효과는 없습니다. Room 대신 인메모리 데이터베이스 또는 SharedPreferences 대신 HashMap이 전형적인 예입니다. 자세한 내용은 Martin Fowler의 테스트 더블 분류에서 확인하세요.
핵심 사항
Fake는 테스트에 적합한 인터페이스의 완전하지만 가벼운 구현체입니다. 이 용어는 Gerard Meszaros(2007)가 저서 “xUnit Test Patterns”에서 소개했습니다. 하드코딩된 응답을 반환하는 stub과 달리, fake는 실행 가능한 코드를 포함합니다: 목록을 정렬하고, 조건에 따라 필터링하고, 레코드를 계산할 수 있습니다. 프로덕션 구현체와의 유일한 차이점은 fake가 인메모리 데이터로 작업하고 실제 I/O 작업을 수행하지 않는다는 것입니다.
주요 장점은 속도입니다. Fake를 사용한 테스트는 디스크, 네트워크 또는 데이터베이스에 액세스하지 않기 때문에 밀리초 단위로 실행됩니다. 인메모리 HashMap은 Room이나 CoreData보다 100~1000배 빠르게 작동합니다. 동시에 fake는 실제 비즈니스 로직(정렬, 필터링, 집계)을 테스트합니다. 이는 stub이 테스트할 수 없는 것들입니다. 왜냐하면 stub은 지시된 것만 반환하기 때문입니다. Fake는 코드가 미리 정해진 답변을 받는 것이 아니라 데이터를 올바르게 처리한다는 확신을 줍니다.
Fake가 Stub보다 선호됩니다 — 테스트 대상 컴포넌트가 데이터에 대해 여러 작업(검색, 필터링, 정렬, 저장)을 수행하는 경우, stub은 각 호출을 개별적으로 구성해야 합니다. Fake는 자체적으로 로직을 포함하므로 테스트는 단순히 메서드를 호출하고 결과를 확인하면 됩니다. IT Sectr에서는 단위 테스트에서 모든 리포지토리에 fakes를 사용합니다. HashMap을 사용하는 fake 리포지토리는 Mockito나 MockK를 설정하지 않고도 90%의 시나리오를 커버합니다.
선택 기준 — 테스트가 무엇을 검증하는지 결정하세요: 상태인지 상호작용인지. 테스트가 상태(작업 결과)를 검증하고 로직을 사용한다면 fake를 사용하세요. 테스트에 로직 없이 입력 데이터만 필요하다면 stub으로 충분합니다. 테스트가 메서드가 호출되었는지 검증한다면 mock을 사용하세요. 하나의 테스트에서 테스트 더블 유형을 혼합하면 이해가 복잡해지고 취약성이 증가합니다.
| 기준 | Fake | Stub | Mock |
|---|---|---|---|
| 로직 보유 | 예 (단순화) | 아니요 | 아니요 |
| 속도 | 높음 | 최대 | 높음 |
| 동작 검증 | 간접적 | 아니요 | 예 (verify) |
| 유지보수 | 인터페이스당 하나의 클래스 | 테스트별 구성 | 테스트별 구성 |
| 현실성 | 높음 (코드 작동) | 낮음 (고정 데이터) | 중간 |
| 위양성 위험 | 낮음 | 중간 | 높음 (취약한 테스트) |
안티패턴: Fake가 아닌 fake — 개발자가 실제로는 stub이나 mock인 객체를 fake라고 부르는 흔한 실수입니다. InMemoryUserRepository에 로직(필터링, 정렬)이 없다면, 그것은 fake가 아니라 인메모리 스토리지를 가진 stub입니다. Fake가 stub과 다른 점은 바로 실행 가능한 로직의 존재입니다. Fake 리포지토리가 단순히 저장된 것을 반환하고 데이터를 처리하지 않는다면, mock이나 stub을 사용하세요.
실용적인 권장사항 — 모든 리포지토리 또는 서비스에 대해 fake로 시작하세요. Fake가 50줄을 초과하면 여러 클래스로 나누세요. Fake가 전혀 필요하지 않은 경우(테스트가 고정 데이터로 단일 시나리오만 검증하는 경우) stub을 사용하세요. 테스트가 특정 매개변수로 메서드가 호출되었는지 검증한다면 mock을 사용하세요. 미리 선택을 최적화하지 마세요. fake를 작성하고, 그것이 과도하다고 판명되면 특정 테스트에서 stub으로 대체하세요.
Room을 위한 Fake 리포지토리 — Android에서 fake의 전형적인 예입니다. UserRepository의 프로덕션 구현은 SQLite 쿼리와 함께 Room DAO를 사용합니다. Fake 버전은 MutableList 또는 HashMap에 데이터를 저장하고 동일한 메서드(getUser(id), saveUser(user), deleteUser(id))를 구현합니다. Fake에는 검색, 필터링 및 정렬 로직이 포함되어 있습니다. 프로덕션 리포지토리와 동일하지만 SQL이 없습니다. 이를 통해 Room 데이터베이스를 설정하지 않고 ViewModel과 UseCase를 테스트할 수 있습니다.
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUser(id: String): User? {
return users.find { it.id == id }
}
override suspend fun saveUser(user: User) {
val index = users.indexOfFirst { it.id == user.id }
if (index >= 0) users[index] = user
else users.add(user)
}
override suspend fun search(query: String): List<User> {
return users.filter {
it.name.contains(query, ignoreCase = true)
}
}
}
Retrofit API를 위한 Fake — MockWebServer(stub이지 fake가 아님) 대신 인메모리 컬렉션에서 데이터를 반환하는 ApiService 구현을 만들 수 있습니다. 차이점: MockWebServer는 HTTP를 가로채서 JSON을 반환하는 반면, fake ApiService는 직렬화 없이 Kotlin 인터페이스 수준에서 작동합니다. Fake는 더 빠르고(JSON 파싱 없음) 디버그하기 쉽습니다(동일한 프로세스에서 실행, 타입 지정). HTTP 의미(상태 코드, 헤더)가 중요하지 않은 테스트에 적합합니다.
Swift에서의 Fake — 프로토콜을 통해 구축됩니다. 프로덕션 클래스는 실제 로직(CoreData, URLSession)으로 프로토콜을 구현합니다. Fake 구조체는 인메모리 스토리지와 단순화된 로직으로 동일한 프로토콜을 구현합니다. Swift는 값 의미론을 가진 언어이므로 fake 구조체는 불변이며 멀티스레드 테스트에서 안전합니다. 이는 Android 아날로그보다 장점을 제공합니다: 인메모리 데이터에 대한 액세스를 동기화할 필요가 없습니다.
protocol UserRepositoryProtocol {
func getUser(id: String) async -> User?
func saveUser(user: User) async
}
final class FakeUserRepository: UserRepositoryProtocol {
private var storage: [String: User] = [:]
func getUser(id: String) async -> User? {
return storage[id]
}
func saveUser(user: User) async {
storage[user.id] = user
}
}
final class UserViewModelTests: XCTestCase {
func test_save_and_load() async {
let fake = FakeUserRepository()
let vm = UserViewModel(repository: fake)
let user = User(id: "1", name: "Alice")
await vm.saveUser(user)
let loaded = await vm.getUser(id: "1")
XCTAssertEqual(loaded?.name, "Alice")
}
}
CoreData를 위한 Fake — iOS 프로젝트에서 description.type = NSInMemoryStoreType을 설정하여 인메모리 NSPersistentContainer를 만들 수 있습니다. 이것은 완전한 CoreData 스택이지만 메모리에서 실행됩니다. 이러한 fake는 SQLite 파일을 만들지 않고 NSFetchRequest, 조건자 및 정렬을 테스트할 수 있습니다. 속도: 인메모리 CoreData의 테스트는 디스크 기반 아날로그보다 5~10배 빠르게 실행됩니다. 단점: 매번 NSManagedObjectModel을 설정해야 합니다.
FakeURLProtocol — iOS에서 네트워크 요청을 가로채기 위한 URLProtocol의 서브클래스입니다. URLProtocol.registerClass(fakeProtocol)을 통해 등록됩니다. 내부적으로 인메모리 URL -> Data 사전을 포함하며 실제 요청 없이 데이터를 반환합니다. Stub과의 차이점: FakeURLProtocol은 요청 본문, 헤더를 확인하고 입력 데이터에 따라 다른 응답을 반환할 수 있습니다. 요청 라우팅 로직을 포함하고 있으므로 이것은 fake입니다.
Test Fixture로서의 Fake — fake 클래스를 공유 테스트 모듈(androidTest/sharedTest 또는 TestSupport)에 배치하세요. 프로젝트의 모든 테스트는 동일한 InMemoryUserRepository를 사용합니다. 이는 각 테스트에서 mock 객체 설정의 중복을 제거하고 일관된 동작을 보장합니다. Fake의 로직을 변경하면 모든 테스트가 동시에 업데이트됩니다. IT Sectr에서는 fake 클래스를 sharedTest/java/com/itSectr/fake/에 저장하고 implementation project(:sharedTest)를 통해 포함시킵니다.
사전 설정 데이터가 있는 Fake — 테스트는 종종 이미 일부 레코드를 포함하는 리포지토리가 필요합니다. 해결책: 팩토리 메서드 fakeWithData(vararg items) 또는 내장된 addDefaultData() 메서드. 팩토리는 fake를 생성하고, 일반적인 데이터로 채우고, 사용 준비가 된 객체를 반환합니다. 이는 테스트의 보일러플레이트를 줄여줍니다: mock 호출을 설정하는 대신, 테스트는 단순히 FakeUserRepository.withUsers(alice, bob)을 호출합니다.
호출 카운트가 있는 Fake — 때로는 상태뿐만 아니라 호출 횟수도 검증해야 할 필요가 있습니다. Fake에는 카운터(saveCallCount, getUserCallCount)를 포함할 수 있습니다. 테스트는 실행 후 카운터를 확인합니다. 이것은 순수 fake(상태 검증)와 mock(상호작용 검증) 사이의 절충안입니다. 카운터는 인수나 호출 순서를 확인하지 않고 횟수만 확인합니다. 인수 검증을 위해서는 mock을 사용하세요.
Callback이 있는 Fake — 비동기 시나리오 테스트를 위해, fake는 각 호출 시 콜백(beforeGetUser, afterSaveUser)을 받을 수 있습니다. 이를 통해 지연, 오류를 시뮬레이션하거나 중간 상태를 확인할 수 있습니다. 이 접근 방식은 UI 로딩 상태 테스트에 유용합니다: fake가 100ms 동안 일시 중지되고, 테스트는 화면에 로더가 표시되는지 확인합니다. 콜백은 프로덕션에 없습니다 — 이것은 순전히 테스트 기능입니다.
자주 묻는 질문
Fake에는 기능적 로직(필터링, 정렬, 카운트)이 포함됩니다. Stub은 로직 없이 미리 정해진 응답만 반환합니다. 객체에 분기(if/else, when)가 있으면 fake입니다. 반환 값만 포함하면 stub입니다. Fake는 유지보수 비용이 더 많이 들지만 더 현실적인 테스트를 제공합니다.
Fake의 로직이 프로덕션 로직과 일치하지 않을 때입니다. 예를 들어, FakeUserRepository가 대소문자를 구분하는 검색을 사용하는데 프로덕션 버전이 대소문자를 구분하지 않는 경우입니다. 테스트는 통과하지만 실제로는 버그가 있습니다. 해결책: fake 로직을 별도로 테스트하거나 간단한 로직(CRUD 작업)이 있는 인터페이스에만 fakes를 사용하세요. 복잡한 로직의 경우 실제 데이터베이스로 통합 테스트를 작성하세요.
인메모리 데이터베이스는 fake의 한 유형입니다. Room.inMemoryDatabaseBuilder()는 프로덕션 데이터베이스처럼 동작하는 인메모리 SQLite를 생성합니다. 이것은 완전한 fake입니다. 하지만 fake는 리포지토리 수준(SQL 없음)과 네트워크 수준(FakeApiService)에도 존재할 수 있습니다. 인메모리 데이터베이스는 로직이 가능한 한 실제와 가까운 fake의 특수한 경우입니다.
네, 하지만 주의가 필요합니다. Fake는 리포지토리(데이터)용, Mock은 AnalyticsTracker(이벤트 검증)용입니다. 계층별 분리: 데이터 계층에는 fake, 분석/로깅 계층에는 mock. 하나의 객체를 동시에 fake와 mock으로 만들지 마세요 — 이는 단일 책임 원칙을 위반하고 테스트를 혼란스럽게 만듭니다.
Fake를 테스트하려면 프로덕션 구현과 동일한 테스트를 사용하세요. save, get, delete를 검증하는 UserRepositoryTest가 있다면, FakeUserRepository와 RealUserRepository로 각각 두 번 실행하세요. 이는 fake가 프로덕션 클래스의 동작을 복제함을 보장합니다. Fake가 다르게 동작하기 시작하면 두 구현 모두에서 테스트가 실패합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.