본문 바로가기
🍎 iOS

[iOS/Swift] Alamofire 프로젝트를 위한 UnitTest

by 틴디 2023. 4. 21.
728x90
반응형

https://www.pexels.com/photo/a-fruit-basket-beside-a-matcha-latte-8471712/

 

URLSession의 dataTask를 프로토콜로 만든 URLSessionProtocol을 URLSession과 NetworkManager의 session의 타입으로 지정해서, 실 사용 앱에서는 기본 URLSession 동작을 그대로 유지하고 테스트 시에는 Mock URLSession을 사용하여 테스트 하는 것이 예시로 많이 나왔다. 

만약 이미 Alamofire을 사용하는 경우 이를 한 번 더 추상화 한 Moya를 사용하는 방법도 있겠지만 우선은 기존 프로젝트의 수정이 최대한 적은 쪽으로 Alamofire로 Unit Test를 시도해 보자!


Session

기존에 사용한 NetworkManager는 Singleton 패턴으로 되어 있다. 외부에서 직접 Session을 주입해주는 것이 아니라 내부에서 request에 필요한 Session을 설정해 주도록 되어 있다.

 

    let sessionManager: Session = {
        let interceptor = BRequestInterceptor()
        let configuration = URLSessionConfiguration.af.default
        configuration.timeoutIntervalForRequest = 30
        configuration.waitsForConnectivity = true
        return Session(configuration: configuration, interceptor: interceptor)
    }()
    func setPostRequest(url: String, params: Parameters?) -> DataRequest {
        return sessionManager.request(APIInfo.hostURL + url, method: .post, parameters: params)
    }

request 시 생성해 준 Session을 사용해서 request를 일으킨 다는 것을 알 수 있고 Session 생성시 URLSessionConfiguration과

RequestInterceptor클래스를 구현한 객체를 인자로 받을 수 있다는 것을 확인할 수 있습니다. 

 

Alamofire의 Session은 'default'로 싱글톤으로 되어 있고 내부에서 URLSession을 사용하고 있습니다.

사용되었던 Initializer를 확인하면 기본적으로  URLSessionConfiguration.af.default를 configuration으로 설정하고 URLSession 설정히 configuration 부분에서 받도록 되어 있습니다. 즉 URLSessionConfiguration을 사용하므로 이와 관련된 값을 변경해서 '실제 앱에 사용' Vs. '테스트에 사용' 할 수 있습니다.

 

URLProtocol

  • URLData의 로딩을 다루는 추상 클래스
  • 프로토콜을 구체화한 URLData로 부터 데이터 로딩을 처리함
  • request를 포착하고 등록된 Mock Data를 반환하도록 해 줄 수 있음
  • 직접적으로 인스턴스를 만들어 사용할 수는 없고 URLProtocol을 서브 클래싱해서 필요한 동작을 지정해 줄 수 있음
  • startLoading 부분에서 원하는 응답값을 response 하도록 할 수 있음
final class MockURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        let initData = MockData.initData.data
        client?.urlProtocol(self, didLoad: initData)
        client?.urlProtocol(self, didReceive: HTTPURLResponse(), cacheStoragePolicy: .allowed)
        client?.urlProtocolDidFinishLoading(self)
    }
    
    override func stopLoading() { }
}

startLoading 부분에서 request 대신 직접 Data객체를 지정해 줍니다. 이때 Data 객체를 외부로 부터 지정받도록 수정할 수도 있습니다.

canInit에서 true(참)을 반환하면 해당 클래스(여기서는 MockURLProtocol이)가 요청을 처리할 수 있음을 나타내고 URLSession 은 해당 프로토콜을 사용하게 됩니다. 

 

protocolClasses

URLSessionConfiguration에서 접근할 수 있는 프로퍼티로 사용자 지정 URLProtocol을 어레이로 받습니다. 

일반 네트워킹 프로콜 기본 집합을 사용자가 정의한 하나 이상의 URLProtocol을 사용할 수 있습니다. 

요청 처리 전 URLSession은 기본 프로토콜을 검색하고 그 다음 지정된 요청을 처리할 수 있는 프로토콜을 찾을 때까지 사용자가 지정한 URLProtocol을 확인합니다.

default는 empty 어레이 입니다. 

 

let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses?.insert(contentsOf: [MockURLProtocol.self], at: 0)
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockURLProtocol.self]

이렇게  URLSessionConfiguration의 protocolClasses로 설정해 주면 정상적인 requestr가 아닌 사용자 지정 URLProtocol을 실행하게 됩니다.

 

NetworkManager

struct NetworkManager {
    private let session: Session
    
    // mock session을 주입 받을 수 있도록 initializer에서 session을 받음
    init(session: Session = .default) {
        self.session = session
    }
    
    // 파라미터, url 설정
    func request<T: Decodable>(url: String, type: T.Type, parameters: Parameters? = nil, completion: @escaping ((T) -> Void)) {
        let request = session.request(url, method: .post, parameters: parameters)
        request.responseString { response in
            switch response.result {
            case .success(let value):
                do {
                    // insert your code
                } catch {
                    // insert your catch handling code
                }
            case .failure(let error):
                // insert your error handling code
            }
        }
    }
}

NetworkManager는 initializer로 Session을 전달 받습니다. 전달 받은 session을 사용해서 request를 합니다. 기본으로 Session 의 값이 default로 설정되어 있어 Session을 직접 주입하지 않는 한 기존 네트워크 동작을 그대로 하게 됩니다. 

Unit Test에서는 Custom URLProtocol을 지정한 Session이 주입되므로 기존 네트워킹을 하지 않고 Mock 데이터를 사용하게 됩니다.

Unit Test

    func testInitData() {
        let expectation = expectation(description: "init 데이터 테스트")
        
        // Given
        let configuration = URLSessionConfiguration.af.default
        configuration.protocolClasses = [MockURLProtocol.self]
        let mockSession = Session(configuration: configuration)
        
        let networkManager = NetworkManager(session: mockSession)
        
        let successCode = "0"
        let url = "insert api url for test"
        
        // When
        networkManager.requestGetInit(url: url, type: AKStructure<Init>.self) { response in
            let requestCode = response.code
            // Then
            expectation.fulfill()
            XCTAssertEqual(successCode, requestCode)
        }
        
        waitForExpectations(timeout: 100)
    }

Given 파트를 보면  URLSessionConfiguration을 생성하고 configuration의 protocolClasses로 custom URLProtocol을 설정해 줍니다. 그 후 Alamofior의 Session객체를 생성하여 Networking을 하는 것이 아닌 Mock data를 사용하도록 해줍니다. 실제로 URL의 api값을 임의로 설정하더라도 UnitTest가 정상적으로 동작하는 것을 확인할 수 있습니다

 

TODO

우선 간단하게 Alamofire + UnitTest가 어떻게 동작하는지 살펴 봤습니다. 이제 기존 프로젝트를 최대한 건들지 않는 선에서 Unit Test를 위한 코드를 넣고, 기존 클래스의 함수를 Testable하게 module화 시킨 후 Unit Test 케이스를 작성해서 성공과 실패를 테스트 해보면 됩니다. 

주로 Alamofire를 한번 더 추상화한 Moya를 사용하기도 하고, 일일이 Mock 데이터 만들어 주는 일을 피하기 위해 Mocker라는 외부 라이브러리도 사용하는 것을 볼 수 있었는데 이에 대해 좀더 알아볼 필요가 있을 거 같습니다. 


참고 사이트 및 도서

https://www.avanderlee.com/swift/mocking-alamofire-urlsession-requests/

 

How to mock Alamofire and URLSession requests in Swift

Mocking Alamofire or URLSession requests can be done without changing your implementation code by making use of a custom URLProtocol in Swift.

www.avanderlee.com

이후 참고할 사이트

https://developer.apple.com/videos/play/wwdc2018/417/

 

Testing Tips & Tricks - WWDC18 - Videos - Apple Developer

Testing is an essential tool to consistently verify your code works correctly, but often your code has dependencies that are out of your...

developer.apple.com

http://minsone.github.io/ios/mac/ios-mock-network-request

 

[iOS] Custom Mock Network Request

Unit Test를 할 때, 네트워크는 어떻게 테스트해야 하나 문제에 봉착합니다. 진짜 네트워크 요청을 해야하는건가 아니면 데이터만 테스트 해야하는가 이렇게 말이죠. 둘 다 테스트를 할 수 있다면

minsone.github.io

 

728x90
반응형

'🍎 iOS' 카테고리의 다른 글

[Xcode/Swift] Configuration 관리하기  (0) 2023.04.26
[iOS/Swift] 앱 뱃지 갯수 변경  (0) 2023.04.26
[Unit Test] Test Double  (0) 2023.04.14
[iOS/Swift] 비동기(async) 테스트  (0) 2023.04.14
[iOS/Swift] 동기(sync) 테스트  (0) 2023.04.13

댓글