-
-
Save ericleiyang/29af1a526d8668a51b8d69a0ae8d15ec to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func testMockGetQuotesSuccess() { | |
// mock response | |
let sampleResponse = | |
""" | |
[ | |
{ | |
"author": "Neale Donald Walsch", | |
"quote": "You are afraid to die, and you're afraid to live. What a way to exist." | |
}, | |
{ | |
"author": "Glenn Parker", | |
"quote": "Growing a beard is the single least amount of work you could ever do to achieve anything." | |
} | |
] | |
""" | |
let mockJSONData = sampleResponse.data(using: .utf8)! | |
MockURLProtocol.error = nil | |
MockURLProtocol.requestHandler = { request in | |
let response = HTTPURLResponse(url: URL(string: QuoteEndpoint.getQuotes.url)!, | |
statusCode: 200, | |
httpVersion: nil, | |
headerFields: ["Content-Type": "application/json"])! | |
return (response, mockJSONData) | |
} | |
// load request | |
let expectation = XCTestExpectation(description: "response") | |
QuoteService.getQuotes(network: MockNetworking(), queue: .main) { result in | |
switch result { | |
case .success(let quotes): | |
XCTAssertEqual(quotes?.count, 2) | |
expectation.fulfill() | |
case .failure(let error): | |
XCTFail(error.localizedDescription) | |
} | |
} | |
wait(for: [expectation], timeout: 10) | |
} | |
struct MockNetworking: Networking { | |
var urlSession: URLSession { | |
let configuration = URLSessionConfiguration.ephemeral | |
configuration.protocolClasses = [MockURLProtocol.self] | |
return URLSession(configuration: configuration) | |
} | |
} | |
class MockURLProtocol: URLProtocol { | |
static var error: Error? | |
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? | |
override class func canInit(with request: URLRequest) -> Bool { | |
return true | |
} | |
override class func canonicalRequest(for request: URLRequest) -> URLRequest { | |
return request | |
} | |
override func startLoading() { | |
// Handle mocking error | |
if let error = MockURLProtocol.error { | |
client?.urlProtocol(self, didFailWithError: error) | |
return | |
} | |
guard let handler = MockURLProtocol.requestHandler else { | |
assertionFailure("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) | |
} | |
} | |
override func stopLoading() { | |
// TODO: Andd stop loading here | |
} | |
} | |
public enum NetworkError: LocalizedError, Equatable { | |
case httpStatusCodeError(statusCode: Int) | |
case generalAPIError(error: String) | |
case failedToGetData | |
case requestFailed | |
} | |
// TODO: Can pass Environment here for more flexibility | |
protocol Networking { | |
var urlSession: URLSession { get } | |
func execute<T: Decodable>(queue: DispatchQueue, | |
_ endpoint: Endpoint, | |
completion: @escaping (Result<T?, NetworkError>) -> Void) | |
} | |
extension Networking { | |
// For Mocking URLSession at unit tests | |
var urlSession: URLSession { | |
return URLSession.shared | |
} | |
func execute<T: Decodable>(queue: DispatchQueue, | |
_ endpoint: Endpoint, completion: @escaping (Result<T?, NetworkError>) -> Void) { | |
let urlRequest = endpoint.urlRequest | |
// TODO: Can return the data request and for managing the requests pool (pause/resume) at UI level | |
urlSession.dataTask(with: urlRequest) { data, response, error in | |
queue.async { | |
handleResponse(endpoint, data: data, response: response, error: error, completion: completion) | |
} | |
}.resume() | |
} | |
private func handleResponse<T: Decodable>(_ endpoint: Endpoint, | |
data: Data?, | |
response: URLResponse?, | |
error: Error?, | |
completion: @escaping (Result<T?, NetworkError>) -> Void) { | |
// check error | |
if let error = error { | |
return completion(.failure(.generalAPIError(error: error.localizedDescription))) | |
} | |
guard let httpResponse = response as? HTTPURLResponse else { | |
return completion(.failure(.requestFailed)) | |
} | |
// check for http status error code first | |
guard (200 ... 299).contains(httpResponse.statusCode) else { // Status code can be changed, range 200~299 is for the py file | |
return completion(.failure(.httpStatusCodeError(statusCode: httpResponse.statusCode))) | |
} | |
// check data | |
guard let data = data else { | |
return completion(.failure(.failedToGetData)) | |
} | |
guard let contentType = httpResponse.allHeaderFields["Content-Type"] as? String, | |
contentType == "application/json" else { | |
let content = String(data: data, encoding: .utf8) | |
return completion(.success(content as? T)) | |
} | |
do { | |
// Use endpoint decoder here for more flexibility | |
let decodedObject = try endpoint.decoder.decode(T.self, from: data) | |
completion(.success(decodedObject)) | |
} catch { | |
completion(.failure(.requestFailed)) | |
} | |
} | |
} | |
// TODO: Can adopts more different networking instances to suit different requirements, like GraphQL networking | |
struct DefaultNetworking: Networking {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment