면접관이 테스트 더블에 대해 물었을 때 어처구니없는 제 답변은 이랬습니다.
"두 번 테스트하는 건가요,,?" 다시 생각해 봐도 부끄러운 것 같습니다.(뭐라도 대답을 해야 할 거 같은 기분에 그랬는데 앞으로는 솔직히 모르겠습니다라고 답변 하는 게 더 좋을 것 같네요)
집에 돌아가는 길 GPT에 검색해 보니 예상과는 전혀 다른 키워드들이 나왔습니다. 평소에 혼동해서 사용하던 단어를 바로잡을 좋은 기회이기도 해 정리해보려 합니다.
테스트 더블?
A test double is software used in software test automation that satisfies a dependency so that the test need not depend on production code. A test double provides functionality via an interface that the software under test cannot distinguish from production code.
테스트 더블은 소프트웨어 테스트 자동화에 사용되는 소프트웨어로, 테스트가 프로덕션 코드에 의존할 필요가 없도록 종속성을 충족합니다. 테스트 더블은 테스트 대상 소프트웨어가 프로덕션 코드와 구별할 수 없는 인터페이스를 통해 기능을 제공합니다.
from. wikipedia
소프트웨어 테스트 자동화에 사용되는 소프트웨어,,, 아직은 모호한 것 같습니다. 곧바로 GPT에게도 물어봤습니다.
GPT의 설명이 훨씬 와닿는 것 같습니다. 소프트웨어 테스트에서 실제 객체의 역할을 대신하는 객체로 이해할 수 있겠네요! (주연 배우 대신 위험한 장면을 수행하는 스턴트 더블에 비유를 해주다니,,, 찰떡이네요)
TCA의 Dependency에 정의하는 previewValue, testValue 등을 테스트 더블로 볼 수 있을 것 같습니다. 테스트를 위한, 프리뷰를 위한 테스트 더블! 이렇게 이야기 할 수 있겠네요.(아래에서 기술하겠지만 요건 Stub으로 볼 수 있겠네요)
테스트 더블의 목적
GPT 선생님의 말을 인용한 3가지 주요 목적은 다음과 같습니다.
- 독립성 보장: 테스트 대상 코드가 다른 컴포넌트에 의존하지 않도록 하기 위해 사용.
- 예측 가능한 테스트 환경 구성: 테스트에 필요한 데이터를 제어하거나 일정한 결과를 반환하도록 설정.
- 외부 시스템 대체: 네트워크 요청이나 DB 호출을 테스트할 때, 실제 시스템 대신 테스트 더블을 사용해 부작용 방지.
간단한 코드 예시를 통해 살펴보겠습니다.
/// 실제 객체
class RealRepository: Repository {
func updateMintol(_ mintol: Mintol) async -> Mintol {
let networkClient = NetworkClient()
let newMintol = await networkClient.update(mintol)
return newMintol
}
}
/// 테스트 더블 객체
class TestDoubleRepository: Repository {
// 1. 독립성 보장: 다른 컴포넌트(NetworkClient)에 의존하지 않음.
// 3. 외부 시스템 대체: NetworkClient를 사용하지 않기 때문에 부작용을 방지 가능.
func updateMintol(_ mintol: Mintol) async -> Mintol {
// 2. 예측 가능한 테스트 환경 구성: 일정한 결과를 반환
return mintol
}
}
테스트 진행 시 실제 객체가 아닌, 테스트 더블 객체를 사용해 안전하고 예측 가능한 테스트가 가능해집니다. 저는 백엔드 서버 구축이 완료되지 않았거나, DTO 테스트를 위해 자주 활용하고 있었던 것 같습니다. (물론 테스트 더블 개념은 몰랐지만 주워 들은 것으로)
추가로 인터페이스를 만들어 채택하고 있기 때문에 객체를 갈아끼워주는 것 만으로 테스트 할 수 있습니다!
테스트 더블의 종류
테스트 더블은 5가지 종류가 있습니다. 평소에 혼용해서 사용한 mock, stub, dummy 등에 모두 의미가 있었다니,, 하나하나 살펴보겠습니다.
1. Dummy 더미
- 실제로 사용되지 않는 객체로, 단순히 매개변수를 채우기 위해 사용.
- 말 그대로 가짜 객체이기 때문에, 실제로 사용되진 않지만 호출용으로 사용.
- ex) 함수의 매개변수로 전달되지만 내부에서 호출되지 않음.
// Dummy: LoggerProtocol이 필요하지만, log를 찍을 필요는 없을 때 사용.
struct DummyLogger: LoggerProtocol {
func log(_ message: String) {} // 구현하지 않음
}
2. Fake 페이크
- 간단한 구현체로 실제 동작을 흉내내지만 완전하지 않은 객체.
- 실제 객체는 복잡하지만 가볍게 구현해 테스트를 하기 위한 목적.
- ex) 메모리에 데이터를 저장하는 가짜 데이터베이스
// Fake: DB의 실제 구현체는 복잡하지만, 테스트를 위해 간단히 작성
class FakeDatabase: DatabaseProtocol {
private var storage: [String: String] = [:]
// 실제로는 훨씬 많은 코드가 들어갈 것,,,
func save(key: String, value: String) {
storage[key] = value
}
// 실제로는 훨씬 많은 코드가 들어갈 것,,,
func fetch(key: String) -> String? {
return storage[key]
}
}
3. Stub 스텁
- 미리 정해진 응답을 제공하는 객체.
- 주로 메서드 호출에 대해 고정된 값을 반환하거나 특정 동작을 모방.
- 테스트가 예측한대로 진행되는지 검증하는데 사용.
// Stub: 테스트를 위해 항상 고정된 값을 반환.
class StubWeatherService: WeatherServiceProtocol {
func fetchWeather(city: String) -> String {
return "Sunny" // 항상 고정된 값 반환
}
}
4. Spy 스파이
- 호출된 내용을 기록하여 이후 호출 여부나 전달된 값을 검증할 수 있는 객체.
// Spy: 호출된 내용을 기록
class SpyLogger: LoggerProtocol {
var loggedMessages: [String] = [] // 기록을 보관할 컬렉션
func log(_ message: String) {
loggedMessages.append(message) // log 기록 추가
}
}
5. Mock 목
- 예상된 동작을 검증할 수 있도록 설정된 객체.
- 테스트 대상 코드의 상호작용을 확인.
- 실제 객체를 모방한 객체.
- 호출 횟수나 호출된 매개변수를 검증하는데 유용.
// Mock: 내부에서 동작을 검증하고 상호작용을 확인
class MockAuthenticationService: AuthenticationServiceProtocol {
var loginCalled = false
func login(username: String, password: String) -> Bool {
loginCalled = true
return username == "test" && password == "password" // 동작 검사
}
}
정리
테스트 더블은 안정적이고 체계적인 테스트를 목표로 두고 있습니다. 각 객체의 사용 맥락에 맞는 타입을 선택하고 명확한 의도를 전달해 테스트 더블을 활용해봐야겠습니다.(앞으로 아무렇게나 네이밍하지 말자!)
참고 링크
'CS' 카테고리의 다른 글
책임과 역할, 비슷한 듯 다른 두 개념 (0) | 2025.02.03 |
---|---|
추상화와 일반화, 비슷한듯 다른 두 개념 (1) | 2025.02.02 |
스레드와 메모리,, 비슷한 거 아니었나요? (0) | 2025.01.15 |