[iOS] Test Doubles가 뭐야?

Madeline👩🏻‍💻·2024년 7월 5일
1

iOS study

목록 보기
58/61
post-thumbnail

들어가며,

왜 근데 더블 테스트도 아니고, 테스트 더블이지?

"테스트 더블"이라는 용어는 연극이나 영화에서 주연 배우를 대신하는 "더블"에서 유래되었으며, 소프트웨어 테스트에서 실제 객체를 대신하여 테스트를 돕는 모든 형태의 객체를 의미합니다. 이는 모의 객체(Mock), 스텁(Stub), 페이크(Fake), 스파이(Spy), 더미(Dummy) 등의 다양한 형태로 구분되며, 각기 다른 테스트 시나리오에서 사용됩니다.

테스트에 사용되는 대역배우 느낌인 것이다~

Test Doubles가 뭐야

테스팅하는 과정에서, 한 컴포넌트가 다른 컴포넌트에 의존할 때가 많다. 예를 들어, 회원가입 기능은 UI요소에 의존할 수 있다. 그러나 이러한 의존 요소들이 아직 완전히 개발되지 않았거나, 특정 테스트 환경에 적합하지 않을 수 있다. 이런 상황에서는 실제 컴포넌트를 테스트 더블로 대체하여 테스트를 진행할 수 있다.

자동화 테스트!!

  • 실제 상황과는 다르게, 단순하게 정의해서 사용하는 객체, 로직 → Test Double

  • 테스트할 때, 실제 객체 대신 사용하는 객체

  • 실제 객체와 같은 인터페이스를 사용하여 구현

  • 실제 소프트웨어의 구성 요소를 대체하는 역할

이러한 더블은 컴포넌트의 동작을 모방하여, 의존성 없이 코드의 개별 기능을 테스트할 수 있게 한다. 유닛 테스트 시에, 특정 기능을 독립적으로 검증하려면 다른 컴포넌트나 복잡한 입력 데이터를 필요로 할 수 있다. 이때 테스트 더블이 그 간격을 메워준다.

종류

stub

  • 하드코딩으로 미리 가공된 데이터를 제공하는 애들, 데이터에 nil 포함될 수 있음
  • 미리 정해진 응답을 반환하는 기본 구현
  • 목적: indirect input 검증하기 위함
  • ex) 네트워크 요청 없이 미리 정의된 HTTP 응답을 반환하는 HTTP 클라이언트

fake

  • 구현된 로직은 있지만, 리얼 코드에 비해선 매우 단순한 로직을 가진 애들
  • 목적: 단순한 로직 제공
  • ex) 메모리에 데이터를 저장하는 페이크 데이터베이스 구현

mock

  • 행동 기반 테스트, 전체적인 로직에 대한 검증
  • count 값을 통해 해당 로직이 실행되었는지 여부 판단
  • iOS는 써드파티라이브러리 이용 or protocol 선언해두고 리얼 코드/mock 코드를 분리하거나, 상속을 통해 테스트할 메서드를 override하는 케이스도 있음
  • ex) 특정 이메일 메시지를 받는 mock 이메일 서비스

spy

  • 테스트에 대한 정보를 기록하는 애들, 로그 메세지 기록
  • 목적: 어떤 메서드가 호출됨으로써 발생할 수 있는 side effect 조사
  • indirect output을 검증할 필요가 있을 때 사용

dummy

  • 필요하지만, 실제로는 쓰지 않는 애들, 자리표시하는 객체
  • 어떤 인스턴스가 필요한데, 그 안의 프로퍼티 값은 어떤 값이 들어가도 상관 없을 경우

indirect input, output?

  • indirect input: 로컬 파일로부터 읽거나 DB로부터 읽는 작업 (메서드 파라미터 아님)
  • indirect output: print문으로 찍거나 파일쓰기, DB write 작업 (메서드 리턴값 아님)

🧐 왜 쓰나?

  1. 코드와 의존성을 분리하여 테스트할 수 있다.
  2. 실패나 오류가 외부 요인에 의해 발생하지 않도록 보장한다.
  3. 테스트의 효율성을 높이고 특정 시나리오에 집중할 수 있다.
  4. TDD 지원한다.

🤓 언제 쓰나?

  1. 외부 API나 써드파티 서비스의 응답을 시뮬레이션할 때
  2. 에러 조건, 예외 처리, 엣지 케이스를 시뮬레이션할 때
  3. 메모리나 시간이 많이 소요되는 함수의 동작을 최적화할 때
  4. 실제 데이터베이스에 접근하지 않고, 데이터베이스 연산을 테스트할 때
  5. 다양한 사용자 역할, 권한, 인증 상태를 테스트할 때

테스트 더블 예시

실제 네트워크 요청을 사용한 테스트는 시간이 오래 걸리고, 불안정할 수 있다. 이런 경우 네트워크 요청에 대한 테스트 더블을 만들어, 다양한 응답을 시뮬레이션할 수 있다.

// 네트워크 서비스 테스트 더블을 정의
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()

😗 Test Doubles 생성 방법

1. 수동 구현

class MockUserService: UserService {
    func fetchUser(id: String, completion: (User?) -> Void) {
        let mockUser = User(id: id, name: "Mock User")
        completion(mockUser)
    }
}

2. 테스팅 프레임워크 사용

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()
    }
}

3. Mocking 라이브러리 사용

  • Mockito(Java), Moq(.NET), Sinon.js(JavaScript), SwiftyMocky(Swift)
  • 주로 코드 내의 특정 부분을 격리하고 검증하는 데에 사용됨

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)
    }
}

4. 의존성 주입

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)

5. 써드파티 도구 사용

WireMock, MockServer 도구를 사용해서 HTTP stub 할 수 있음

  • 외부 시스템이나 서비스와의 상호작용을 시뮬레이션하고, 통합 테스트를 지원

테스트 더블은 소프트웨어 테스트에서 중요한 역할을 합니다. 의존성이 없는 테스트 환경을 제공하여 효율적이고 신뢰할 수 있는 테스트를 가능하게 합니다. 테스트 더블을 올바르게 사용하면 더 나은 테스트 결과를 얻을 수 있습니다.

💩 레퍼런스

Test double in iOS

https://testsigma.com/blog/test-doubles/

profile
🍎 Apple Developer Academy@POSTECH 2기, 🍀 SeSAC iOS 4기

0개의 댓글