본문 바로가기

iOS 개발/App 개발 관련

[iOS] 모의 URLSession 으로 네트워킹 Test 하기

목차 (눌러서 이동)

 

     

    모의 URLSession 이 필요한 이유

    Network 연결이 필요한 작업을 테스트할 때 항상 실제 network 를 통해 해당 URL 에 접근한다면 너무 많은 시간이 소요된다. 따라서, 실제 network 연결 없이 네트워킹 함수를 테스트 할 수 있어야 한다.

    URL download 함수

    func downloadData(_ session: URLSession, completionBlock: @escaping (Result<Data, Error>) -> Void) {
        if let url = URL(string: /*Put URL that you want to downlaod some data.*/) {
            let task = session.dataTask(with: url, completionHandler: {data , urlresponse, error in
                if let data = data {
                    completionBlock(.success(data))
                }
            })
            task.resume()
        }
    }

     

    위와 같이 어떤 URL 에서 원하는 data 를 내려받는 함수를 작성할 수 있다. 만약 이 함수를 테스트 하기 위해 매번 이를 호출하여 다운로드 한다면 불필요하게 오래 걸리는 작업이 될 것이다.

    URL Session 흉내내기

    URLSession 을 상속하는 Class 만들기

    URLSession 을 상속하는 class 를 만들면 dataTask 함수를 override 하여 실제 네트워킹은 하지 않도록 할 수 있다. (URLSession 은 dataTask 함수를 통해 url 에 있는 data 를 가져온다.)

     

    class URLSessionDataTaskMock: URLSessionDataTask {
        private let closure: () -> Void
        init(closure: @escaping () -> Void) {
            self.closure = closure
        }
        override func resume() {
            closure()
        }
    }
    
    class URLSessionMock: URLSession {
        typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
        var data: Data?
        var error: Error?
        override func dataTask(
            with url: URL,
            completionHandler: @escaping CompletionHandler
            ) -> URLSessionDataTask {
            let data = self.data
            let error = self.error
            return URLSessionDataTaskMock {
                completionHandler(data, nil, error)
            }
        }
    }

     

    URLSessionMock class 를 통해 test 를 작성하자.

     

    func testUsingSimpleMock() {
    	let mockSession = URLSessionMock()
    	mockSession.data = "testData".data(using: .ascii)
    	let exp = expectation(description: "Loading URL")
    	let vc = ViewController()
    	vc.downloadData(mockSession, completionBlock: {data in
    		exp.fulfill()
    	})
    	waitForExpectations(timeout: 10)
    }

     

    위에서 작성한 downloadData 함수를 mockSession 을 넘겨주며 호출한다. downloadData 함수 내에서는 mockSession 의 dataTask 함수를 호출, 실제 url 접근 없이 test 함수에서 만들어낸 가짜 data 를 넘기고 반환하면서 test 를 완료한다. 

    Subclassing 의 문제점

    애플의 공식 URLSession class 를 상속한 뒤 메서드를 override 하여 실제로 network 연결하지 않도록 조작하는 것 이므로,

    애플에서 메서드를 추가한다면 일일이 다시 override 해야 한다.

    Mock URLSession with a protocol

    URLSession 을 상속해서 생기는 문제를 예방하기 위해 protocol 을 선언한 뒤 URLSessionMock 이 이를 채택한다. 이를 채택하면 dataTask() 를 작성해야 하도록 protocol 을 만든다.

     

    protocol URLSessionProtocol {
    	func dataTask(
        	with url: URL, 
            completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
            ) -> URLSessionDataTask
    }

     

    그리고 URLSessionMock 이 URLSession 을 상속하는 것이 아닌 URLSessionProtocol 을 채택하도록 한다.

     

    class URLSessionMock: URLSessionProtocol {
    	typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
    	var data: Data?
    	var error: Error?
    	func dataTask(
    		with url: URL,
    		completionHandler: @escaping CompletionHandler
    	) -> URLSessionDataTask {
    		let data = self.data
    		let error = self.error
    		return URLSessionDataTaskMock {
    			completionHandler(data, nil, error)
    		}
    	}
    }

     

    URLSessionDataTask 역시 protocol 을 선언하여 모의 class 에서 채택하도록 한다.

     

    protocol URLSessionDataTaskProtocol {
    	func resume()
    }
    class URLSessionDataTaskMock: URLSessionDataTaskProtocol {
    	private let closure: () -> Void
    	init(closure: @escaping () -> Void) {
    		self.closure = closure
    	}
    	func resume() {
    		closure()
    	}
    }

     

    그런데 기존의 URLSessionMock 에서 dataTask 함수는 URLSessionDataTask 타입의 객체를 반환하도록 되어 있는데, URLSessionDataTaskProtocol 을 채택하는 것으로 바뀌었기 때문에 xcode 가 에러를 일으킨다.

     

    따라서, dataTask 가 URLSessionDataTaskProtocol 을 반환하도록 한다. (즉, URLSessionProtocol 의 dataTask 의 반환형을 URLSessionDataTaskProtocol 로 바꾸고 URLSessionMock class 의 dataTask 반환형도 따라서 바꾼다.)

     

    protocol URLSessionProtocol {
    	func dataTask(
        	with url: URL, 
        	completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
        ) -> URLSessionDataTaskProtocol
    }
    
    protocol URLSessionDataTaskProtocol {
    	func resume()
    }
    
    class URLSessionDataTaskMock: URLSessionDataTaskProtocol {
    	private let closure: () -> Void
    	init(closure: @escaping () -> Void) {
    		self.closure = closure
    	}
    	func resume() {
    		closure()
    	}
    }
    
    class URLSessionMock: URLSessionProtocol {
    	typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
    	var data: Data?
    	var error: Error?
    	func dataTask(
    		with url: URL,
    		completionHandler: @escaping CompletionHandler
    	) -> URLSessionDataTaskProtocol {
    		let data = self.data
    		let error = self.error
    		return URLSessionDataTaskMock {
    			completionHandler(data, nil, error)
    		}
    	}
    }