"테스트 더블"이라는 용어는 연극이나 영화에서 주연 배우를 대신하는 "더블"에서 유래되었으며, 소프트웨어 테스트에서 실제 객체를 대신하여 테스트를 돕는 모든 형태의 객체를 의미합니다. 이는 모의 객체(Mock), 스텁(Stub), 페이크(Fake), 스파이(Spy), 더미(Dummy) 등의 다양한 형태로 구분되며, 각기 다른 테스트 시나리오에서 사용됩니다.
테스트에 사용되는 대역배우 느낌인 것이다~
테스팅하는 과정에서, 한 컴포넌트가 다른 컴포넌트에 의존할 때가 많다. 예를 들어, 회원가입 기능은 UI요소에 의존할 수 있다. 그러나 이러한 의존 요소들이 아직 완전히 개발되지 않았거나, 특정 테스트 환경에 적합하지 않을 수 있다. 이런 상황에서는 실제 컴포넌트를 테스트 더블로 대체하여 테스트를 진행할 수 있다.
자동화 테스트!!
실제 상황과는 다르게, 단순하게 정의해서 사용하는 객체, 로직 → Test Double
테스트할 때, 실제 객체 대신 사용하는 객체
실제 객체와 같은 인터페이스를 사용하여 구현
실제 소프트웨어의 구성 요소를 대체하는 역할
이러한 더블은 컴포넌트의 동작을 모방하여, 의존성 없이 코드의 개별 기능을 테스트할 수 있게 한다. 유닛 테스트 시에, 특정 기능을 독립적으로 검증하려면 다른 컴포넌트나 복잡한 입력 데이터를 필요로 할 수 있다. 이때 테스트 더블이 그 간격을 메워준다.
실제 네트워크 요청을 사용한 테스트는 시간이 오래 걸리고, 불안정할 수 있다. 이런 경우 네트워크 요청에 대한 테스트 더블을 만들어, 다양한 응답을 시뮬레이션할 수 있다.
// 네트워크 서비스 테스트 더블을 정의
class NetworkServiceTestDouble {
func fetchData(completion: (Result<Data, Error>) -> Void) {
// 응답 성공 시뮬레이션
let successResponse = """
{
"status": "success",
"data": {
"id": "123456",
"name": "Test User"
}
}
""".data(using: .utf8)!
completion(.success(successResponse))
}
func fetchError(completion: (Result<Data, Error>) -> Void) {
// 응답 에러 시뮬레이션
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Network error"])
completion(.failure(error))
}
}
// 테스트 더블 사용 예시
class ViewModel {
let networkService: NetworkServiceTestDouble
init(networkService: NetworkServiceTestDouble) {
self.networkService = networkService
}
func loadData() {
networkService.fetchData { result in
switch result {
case .success(let data):
print("Data received: \(data)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
}
let networkTestDouble = NetworkServiceTestDouble()
let viewModel = ViewModel(networkService: networkTestDouble)
viewModel.loadData()
class MockUserService: UserService {
func fetchUser(id: String, completion: (User?) -> Void) {
let mockUser = User(id: id, name: "Mock User")
completion(mockUser)
}
}
XCTest 프레임워크 사용
import XCTest
class MockURLSession: URLSession {
var cachedUrl: URL?
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
cachedUrl = url
completionHandler(data, response, error)
return URLSessionDataTask()
}
}
https://github.com/MakeAWishFoundation/SwiftyMocky
import SwiftyMocky
protocol UserService {
func fetchUser(id: String, completion: @escaping (User?) -> Void)
}
class UserServiceMock: UserService, Mock {
var matcher: Matcher = Matcher.default
var stubbingPolicy: StubbingPolicy = .wrap
var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst
func fetchUser(id: String, completion: @escaping (User?) -> Void) {
return matcher.call(id, completion)
}
}
protocol NetworkService {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}
class RealNetworkService: NetworkService {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// 실제 네트워크 요청 처리 코드
}
}
class MockNetworkService: NetworkService {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// 모의 네트워크 요청 처리 코드
let data = Data()
completion(.success(data))
}
}
class ViewModel {
let networkService: NetworkService
init(networkService: NetworkService) {
self.networkService = networkService
}
}
let mockService = MockNetworkService()
let viewModel = ViewModel(networkService: mockService)
WireMock, MockServer 도구를 사용해서 HTTP stub 할 수 있음
테스트 더블은 소프트웨어 테스트에서 중요한 역할을 합니다. 의존성이 없는 테스트 환경을 제공하여 효율적이고 신뢰할 수 있는 테스트를 가능하게 합니다. 테스트 더블을 올바르게 사용하면 더 나은 테스트 결과를 얻을 수 있습니다.