Alamofire의 Mock Test해보기

Groot·2023년 2월 6일
0

TIL

목록 보기
127/153
post-thumbnail
post-custom-banner

TIL

🌱 난 오늘 무엇을 공부했을까?

📌 Alamofire의 Mock Test해보기

📍 Mock Session 객체란?

  • 인터넷 연결과 상관없이 네트워크 테스트를 진행할 수 있도록 만드는 가짜 Session
  • Mock Session으로 Session을 바꿔주는 방법으로 Network Manager의 테스트를 간단하게 진행했다.

📍 Alamofire에서 Session을 어떻게 쓰는가?

  • Alamofire의 github에서 session 부분을 보면 Session이란 부분이 있다.
  • 현재 싱글톤으로 Session이란 객체를 사용하고 있는데, 이 객체 내부에 session이란 이름의 URLSession 타입을 가지고 있다.
  • Alamofire도 결국엔 URLSession을 사용해서 요청을 하기 때문에 이 URLSession을 의존성 주입으로 변경해보자.

📍URLSession을 변경해보기

  • URLSession의 init을 위해선 URLSessionConfiguration도 init을 해줘야 한다.

    • URLSession의 이니셜라이저: init(configuration: URLSessionConfiguration)
  • URLSessionConfiguration에 대한 내용은 공식문서에 이렇게 나온다.

    • URLSession에 대한 동작 및 정책을 정의하는 구성 개체
    • URLSessionConfiguration의 init은 Deprecated 이기 때문에 따로 init은 불가능하다. 그래서 Alamofire에서도 URLSessionConfiguration.default를 사용한다.
  • URLSessionConfiguration.default을 사용하되 기존의 객체를 변경해주는 방법으로 Mock을 만들 수 있다.

  • Urlsessionconfiguration 공식문서의 "Supporting Custom Protocols" 부분을 보면 된다.

    var protocolClasses: [AnyClass]?
    // 세션에서 요청을 처리하는 추가 프로토콜 하위 클래스의 배열
    class URLProtocol
    // 프로토콜별 URL 데이터 로드를 처리하는 추상 클래스
  • MockURLProtocol을 만들어서 URLSessionConfiguration.default의 protocolClasses에 넣어주는 방법으로 MockSession을 사용해보자.

📍 MockURLProtocol 만들기

  • URLProtocol공식문서를 보면 여러가지 메서드가 많다
  • 지금은 테스트에 필요한 부분만 override 하는 방법으로 구현해보자.
import Foundation

final class MockURLProtocol: URLProtocol {
    // 1. 프로토콜 하위 클래스가 지정된 요청을 처리할 수 있는지 여부를 결정 -> 당연히 true
        override class func canInit(with request: URLRequest) -> Bool {
            return true
        } 

    // 2. 지정된 요청의 정식 버전을 반환 -> 이건 따로 지정할 필요없이 요청을 그대로 반환한다.
        override class func canonicalRequest(for request: URLRequest) -> URLRequest {
            return request
        } 

    // 3. 요청의 프로토콜별 로드를 시작 -> 이 부분에서 우리가 전달해야할 데이터를 주입해줘야 한다.
        override func startLoading() {
            // 3.1 내가 테스트에 사용할 MockData
            let data = MockNetworkData(fileName: "Box_Office_Sample").data! 
            
            // client란 ?? 
            // 정의(var client: URLProtocolClient?) 프로토콜이 URL 로드 시스템과 통신하는 데 사용하는 개체

            // 아래부터 URLProtocolClient의 메서드를 사용해서 데이터를 전달하는 부분.
            
            // 3.2 프로토콜 구현이 일부 데이터를 로드했음을 클라이언트에 알림 (Required) -> 데이터 전달.
            client?.urlProtocol(self, didLoad: data) 
             // 3.3 프로토콜 구현이 요청에 대한 응답 개체를 생성했음을 클라이언트에 알림 (Required)
            client?.urlProtocol(self, didReceive: HTTPURLResponse(), cacheStoragePolicy: .allowed)
            // 3.4 프로토콜 구현이 로드를 완료했음을 클라이언트에 알림 (Required)
            client?.urlProtocolDidFinishLoading(self)
        } 

        // 요청의 프로토콜별 로드를 중지
        override func stopLoading() {} 
}
  • 여기서 중요한 부분은 client부분에서 데이터를 전달, 응답을 생성, 다시 응답을 보내는 부분이 우리가 데이터 Session을 바꿔준 이유라고 볼 수 있다.
  • URLProtocol의 Task나, Response를 사용해서 상세하게 테스트도 가능하다.

📍 NetworkManager Session을 의존성 주입해주기

struct NetworkManager {
    private let session: Session
    // 기본적으론 Session.default를 사용하지만, Mock Session을 넣을 수 있도록 의존성을 주입받도록 설정.
    init(session: Session = Session.default) {
            self.session = session
    }
    
    func request(url: String, parameters: Parameters? = nil) -> Observable<Data> {
        return Observable<Data>.create { observer in
            session
                .request(url, method: .get, parameters: parameters)
                .validate(statusCode: 200...299)
                .response { (response) in
                    guard let optionalData = response.value,
                          let data = optionalData else { return }
                    
                    observer.onNext(data)
                    observer.onCompleted()
                }
            
            return Disposables.create()
        }
    }
}

📍 실제 UnitTest

  • 공식문서에 URLProtocol의 하위 클래스는 init하지 말라고 나온다. 다운로드가 시작되면 시스템이 적절하게 생성한다고 함.
import XCTest
import Foundation
import RxSwift
import Alamofire
@testable import MyBoxOffice

final class NetworkManagerTests: XCTestCase {
    private let disposeBag = DisposeBag()
    
    func test_MockSessionNetworkManager_Daily_BoxOffice_request() {
        let expectation = expectation(description: "비동기 처리")
        //given
        let configuration = URLSessionConfiguration.af.default 
        configuration.protocolClasses?.insert(contentsOf: [MockURLProtocol.self], at: 0) 
        let mockSession = Session(configuration: configuration)
        // 만들어준 mockSession을 NetworkManager init에서 의존성을 주입하는 방법으로 Session을 가로챈다.
        let networkManager = NetworkManager(session: mockSession)
        var movieNm: String?
        let url = "MockDailyBoxOfficeURL"
        networkManager.request(url: url)
            .decode(type: BoxOfficeDTO.self, decoder: JSONDecoder())
            .subscribe { event in
                guard let data = event.element else { return }
                
                movieNm = data.boxOfficeResult.dailyBoxOfficeList.first?.movieNm
                expectation.fulfill()
            }.disposed(by: disposeBag)
        
        //when
        let result = "경관의 피"
        
        //then
        waitForExpectations(timeout: 3)
        XCTAssertEqual(result, movieNm)
    }
}
  • let networkManager = NetworkManager(session: mockSession) 이 부분을 위해서 지금까지 모든걸함..

📍 후기

  • 지금 만든 MockURLProtocol을 조금 범용적으로 만들면 꼭 Alamofire가 아닌 다른 프로젝트에서도 MockSession이 필요할 때 계속 사용할 수 있다고 생각이 든다. 🤔

https://leeari95.tistory.com/71
https://github.com/WeTransfer/Mocker#mock-callbacks
https://github.com/WeTransfer/Mocker
https://www.avanderlee.com/swift/mocking-alamofire-urlsession-requests/
https://github.com/Alamofire/Alamofire

profile
I Am Groot
post-custom-banner

0개의 댓글