목차 (눌러서 이동)
모의 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)
}
}
}
'iOS 개발 > App 개발 관련' 카테고리의 다른 글
[iOS] AppDelegate, SceneDelegate 의 역할, WWDC2019 (iOS 13~) (0) | 2021.10.29 |
---|---|
[iOS] Stanford iOS Lecture - MVVM (cs193p) (0) | 2021.08.31 |
[iOS] TDD(Test Driven Development) Tutorial (0) | 2021.07.16 |
[iOS] WWDC 2019 Testing in Xcode (0) | 2021.07.12 |
[iOS] Unit & UI Testing (0) | 2021.07.10 |