Skip to content

Instantly share code, notes, and snippets.

@ericleiyang
Created October 11, 2020 08:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericleiyang/29af1a526d8668a51b8d69a0ae8d15ec to your computer and use it in GitHub Desktop.
Save ericleiyang/29af1a526d8668a51b8d69a0ae8d15ec to your computer and use it in GitHub Desktop.
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