WWDC 18 - Testing Tips & Tricks - 1
2023. 3. 24. 18:58ㆍIOS/WWDC
반응형
WWDC 18 - Testing Tips & Tricks - 1
Testing network requests
- 앱에서 네트워킹을 통해 무언가를 받아올 경우 위와 같은 형태로 진행되며
- 몇가지의 메소드를 통해 위 과정을 처리합니다.
func loadData(near coord: CLLocationCoordinate2D) {
let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { self.handleError(error); return }
do {
let values = try JSONDecoder().decode([PointOfInterest].self, from: data)
DispatchQueue.main.async {
self.tableValues = values
self.tableView.reloadData()
}
} catch {
self.handleError(error)
}
}.resume()
}
- 위와 같이 좌표를 통해 무언가 값을 받아올 경우
URL
을 생성하고dataTask
메소드를 통해URLSessionDataTask
를 만들어resume
메소드로 동작합니다. - 그리고 받아온
error
,response
,data
를 통해 에러를 처리하거나 데이터를 deconding하여 뷰를 업데이트 할 수 있습니다. - 우리는 위에서 보았던 사진의 한 부위씩 유닛테스팅을 하여야합니다.
Prepare URLRequest, Parse Response
- 기존 위에서 작성하였던 코드를 여러개의 메소드로 나누어서 관리하면 따로 테스트하기가 용이해집니다.
struct PointsOfInterestRequest {
func makeRequest(from coordinate: CLLocationCoordinate2D) throws -> URLRequest {
guard CLLocationCoordinate2DIsValid(coordinate) else {
throw RequestError.invalidCoordinate
}
var components = URLComponents(string: "https://example.com/locations")!
components.queryItems = [
URLQueryItem(name: "lat", value: "\(coordinate.latitude)"),
URLQueryItem(name: "long", value: "\(coordinate.longitude)")
}
]
return URLRequest(url: components.url!)
}
}
- 하나는 Prepare URLRequest를 테스트하기 위한 메소드로 기존 코드와 유사한 형태지만
URLRequest
를 반환하고 좌표가 올바르지 않을 경우invalidCoordinate
에러를 반환
func parseResponse(data: Data) throws -> [PointOfInterest] {
return try JSONDecoder().decode([PointOfInterest].self, from: data)
}
- 하나는
Data
를 원하는 형태로 decoding해주는 메서드로 Parse Response의 역할 중 하나입니다. - 위와 같이 메서드로 나누어 세부적인 테스트케이스 작성이 가능해졌습니다.
class PointOfInterestRequestTests: XCTestCase {
let request = PointsOfInterestRequest()
func testMakingURLRequest() throws {
let coordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893)
let urlRequest = try request.makeRequest(from: coordinate)
XCTAssertEqual(urlRequest.url?.scheme, "https")
XCTAssertEqual(urlRequest.url?.host, "example.com")
XCTAssertEqual(urlRequest.url?.query, "lat=37.3293&long=-121.8893")
}
}
makeRequest
를 통해 올바른 url값을 가지고 있는지 실제 좌표 값은 정확히URL query
에 들어가 있는지에 대한 테스트를 작성할 수 있습니다.
class PointOfInterestRequestTests: XCTestCase {
func testParsingResponse() throws {
let jsonData = "[{\"name\":\"My Location\"}]".data(using: .utf8)!
let response = try request.parseResponse(data: jsonData)
XCTAssertEqual(response, [PointOfInterest(name: "My Location")])
}
}
- 또 원하는 값으로 JSON Decoding이 되고 있는지에 대한 여부도 테스트가 가능합니다.
두 메서드는 Error를 반환하도록
throws
가 있지만XCTest
의 기능으로do-catch
문을 사용하지 않아도 위와 같이 사용이 가능합니다
Create URLSession Task
- 위에서 구현한 내용을 토대로 프로토콜을 만들 수 있습니다.
protocol APIRequest {
associatedtype RequestDataType
associatedtype ResponseDataType
func makeRequest(from data: RequestDataType) throws -> URLRequest
func parseResponse(data: Data) throws -> ResponseDataType
}
- 해당 프로토콜을 통해
URLSession DataTask
와 관련된 클래스를 아래와 같이 또 만들 수 있습니다.
class APIRequestLoader<T: APIRequest> {
let apiRequest: T
let urlSession: URLSession
init(apiRequest: T, urlSession: URLSession = .shared) {
self.apiRequest = apiRequest self.urlSession = urlSession
}
}
- 해당
T
제너릭 타입은 위에서 구현한PointsOfInterestRequest
로 생성할 수 있게 됩니다.
class APIRequestLoader<T: APIRequest> {
func loadAPIRequest(requestData: T.RequestDataType,
completionHandler: @escaping (T.ResponseDataType?, Error?) -> Void) {
do {
let urlRequest = try apiRequest.makeRequest(from: requestData)
urlSession.dataTask(with: urlRequest) { data, response, error in
guard let data = data else { return completionHandler(nil, error) }
do {
let parsedResponse = try self.apiRequest.parseResponse(data: data)
completionHandler(parsedResponse, nil)
} catch {
completionHandler(nil, error)
}
}.resume()
} catch {
return completionHandler(nil, error)
}
}
}
- 앱 내에서 처음 구현한
dataTask
를 해당 타입에서 다시 구현을 할 수 있습니다.
- 이를 통해 기존의 하위레벨 단계의 유닛테스트를 더욱 큰 단계의 테스트로 바꿀 수 있습니다.
APIRequestLoader
를 이용하여 Prepare URLRequest, Create URLSession Task, Parse Response까지 테스트를 할 수 있도록 코드가 구현되었습니다.
How to Use URLProtocol
URLSession
은 네트워크 통신을 하기 위해URLSessionDataTask
와 같은 상위 수준 API를 제공합니다.
- 하지만 더욱 낮은 구현부에서는 네트워크 연결 열기, 네트워크 응답 작성, 응답 요청 등 하위수준의 API
URLProtocol subclasses
가 존재합니다. URLProtocol
은 URL로딩시스템에 대한 확장을 제공하기 위해 서브클래싱되어 설계되었습니다.
Foundation
은 HTTPS와 같은 프로토콜에 대해서 하위 클래스를 제공합니다- 다만 우리는 테스트에 필요한 요청 자체를 모의(Mock)으로 만들어 제공할 수 있는 프로토콜을 구현하여 재정의 할 수 있습니다.
- 위와 같이
URLProtocol
은URLProtocolClient
를 통해 시스템에 진행 상황을 시스템에 전달하는데 이를MockProtocol
을 구현하여 해결할 수 있습니다.
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() {
}
override func stopLoading() {
}
}
- 위와 같이
URLProtcol
을 상속받아 서브클래스로MockProtocol
을 구현할 수 있습니다.
override class func canInit(with request: URLRequest) -> Bool {
return true
}
- 해당 메소드는
request
에 대한 관심여부라고 볼 수 있습니다. 만약 이것이true
라면 시스템을 통한 모든request
는MockURLProtocol
을 통해서 진행됩니다.
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
- 해당 메소드는
request
에 관해 표준 버전을 반환해주는 역할을 합니다. - 이 외에
startLoading
과stopLoading
은 이름 그대로의 역할을 수행합니다. request
에 관해canonicalRequest
를 구현을 하지만 대부분의 작업은startLoading
에서 발생합니다.
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
- 테스트가 해당
URLProtocol
에 연결할 수 있는 방법으로는requestHandler
를 활용하는것입니다. URLSession
의 작업이 시작(resume)되면 시스템은URLProtocol
의 하위 클래스를 인스턴스화 하여URLRequest
,URLProtocol Client
인스턴스를 제공합니다.(startLoading
메소드가 실행되기 이전)- 이후
startLoading
메소드가 실행되는데 위에서 구현한reuqestHandler
를 가져와URLRequest
,Data
를 매개변수로 활용합니다.
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("Received unexpected request with no handler set")
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
- 우리는
handler
를 통해 받은 임의로 준response
와data
를URLProtocolClient
의 인스턴스인client
에 주어서URLSessionDataTask
로 전달합니다.
- 테스트 요청 취소와 관련된 작업을 테스트하고 싶을 경우 위와 마찬가지로
stopLoading
메소드를 통해 구현할 수도 있습니다.
- Stub프로토콜을 이용하여 테스트케이스를 작성하면
class APILoaderTests: XCTestCase {
var loader: APIRequestLoader<PointsOfInterestRequest>!
override func setUp() {
let request = PointsOfInterestRequest()
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
loader = APIRequestLoader(apiRequest: request, urlSession: urlSession)
}
}
- 이전에 구현한
APIRequestLoader
의 인스턴스를 만들고URLProtocol
을 사용하도록URLSession
을 구상할 수 있습니다.
class APILoaderTests: XCTestCase {
func testLoaderSuccess() {
let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893)
let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)!
MockURLProtocol.requestHandler = { request in
XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true)
return (HTTPURLResponse(), mockJSONData)
}
let expectation = XCTestExpectation(description: "response")
loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in
XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")])
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
}
- 이전의
MockURLProtocol
의requestHandler
에 Stub응답과 데이터를 넣음으로loader
즉APIRequestLoader
의 메소드를 테스트할 수 있습니다. - 위와 같이 세분화하고 Mock을 만들고 테스트코드를 작성한다면
URLSessionDataTask
의resume
메서드를 실수로 쓰지 않았다면 테스트에 실패하므로 좋은 테스트코드입니다. - 이것은 UI테스트에 관해서도 굉장히 유용한 결과를 낼 수 있습니다.
( ~ 13 : 40)
반응형
'IOS > WWDC' 카테고리의 다른 글
WWDC22 - Design protocol interfaces in Swift - 1편 (0) | 2023.07.25 |
---|---|
WWDC21 - Your guide to keyboard layout (0) | 2023.04.29 |
WWDC19 - Advances in Collection View Layout (0) | 2023.04.21 |
WWDC18 - Testing Tips and Tricks - 3 (0) | 2023.04.18 |
WWDC18 - Testing Tips and Tricks - 2(Working with notifications) (0) | 2023.04.12 |