Created
August 25, 2023 10:04
-
-
Save IniongunIsaac/ebc22e20783ba5cd3853bf68fee5b553 to your computer and use it in GitHub Desktop.
Simple Networking Setup with XCTests
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
final class MockURLProtocol: URLProtocol { | |
static var data: Data? | |
static var error: Error? | |
static var urlResponse: URLResponse? | |
override class func canInit(with request: URLRequest) -> Bool { | |
true | |
} | |
override class func canonicalRequest(for request: URLRequest) -> URLRequest { | |
request | |
} | |
override func startLoading() { | |
if let signupError = MockURLProtocol.error { | |
client?.urlProtocol(self, didFailWithError: signupError) | |
} | |
if let urlResponse = MockURLProtocol.urlResponse { | |
client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .notAllowed) | |
} | |
if let data = MockURLProtocol.data { | |
client?.urlProtocol(self, didLoad: data) | |
} | |
client?.urlProtocolDidFinishLoading(self) | |
} | |
override func stopLoading() {} | |
} |
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
final class RemoteDatasource: RemoteDatasourceProtocol { | |
private let urlSession: URLSession | |
init(urlSession: URLSession) { | |
self.urlSession = urlSession | |
} | |
func makeRequest<T: Codable>( | |
responseType: T.Type, | |
requestMethod: RemoteHttpMethod, | |
remotePath: RemotePath, | |
parameters: [String : Any], | |
completion: @escaping (Result<T, RemoteDatasourceError>) -> Void | |
) { | |
guard let requestURL = URL(string: remotePath.url) else { | |
completion(.failure(.invalidURL)) | |
return | |
} | |
var urlRequest = URLRequest(url: requestURL) | |
urlRequest.httpMethod = requestMethod.rawValue | |
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") | |
if requestMethod == .post { | |
do { | |
let requestBody = try JSONSerialization.data(withJSONObject: parameters) | |
urlRequest.httpBody = requestBody | |
} catch { | |
completion(.failure(.encodingFailure(reason: error.localizedDescription))) | |
} | |
} | |
urlSession.dataTask(with: urlRequest) { data, urlResponse, error in | |
if let httpURLResponse = urlResponse as? HTTPURLResponse { | |
let statusCode = httpURLResponse.statusCode | |
if (400...499).contains(statusCode) { | |
completion(.failure(.resourceNotFound)) | |
} | |
if statusCode >= 500 { | |
completion(.failure(.serverFailure)) | |
} | |
return | |
} | |
if let error { | |
completion(.failure(.requestFailure(reason: error.localizedDescription))) | |
return | |
} | |
if let data { | |
do { | |
let response = try JSONDecoder().decode(T.self, from: data) | |
completion(.success(response)) | |
} catch { | |
completion(.failure(.decodingFailure(reason: error.localizedDescription))) | |
} | |
return | |
} | |
}.resume() | |
} | |
} |
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
enum RemoteDatasourceError: Error, Equatable { | |
case invalidURL | |
case requestFailure(reason: String) | |
case decodingFailure(reason: String) | |
case encodingFailure(reason: String) | |
case resourceNotFound | |
case serverFailure | |
var description: String? { | |
switch self { | |
case .invalidURL: | |
return "Bad Request URL" | |
case .requestFailure(reason: let reason): | |
return "Unable to perform request, please try again.\nReason: \(reason)" | |
case .decodingFailure(reason: let reason): | |
return "Unable to read data from server, please try again.\nReason: \(reason)" | |
case .encodingFailure(reason: let reason): | |
return "Unable to send request data, please try again.\nReason: \(reason)" | |
case .resourceNotFound: | |
return "Unable to locate resource on the server, please try again or contact customer support." | |
case .serverFailure: | |
return "Unable to contact the server, please try again." | |
} | |
} | |
} |
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
protocol RemoteDatasourceProtocol { | |
func makeRequest<T: Codable>( | |
responseType: T.Type, | |
requestMethod: RemoteHttpMethod, | |
remotePath: RemotePath, | |
parameters: [String : Any], | |
completion: @escaping (Result<T, RemoteDatasourceError>) -> Void | |
) | |
} |
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
import XCTest | |
//@testable import AppName | |
final class RemoteDatasourceTests: XCTestCase { | |
private var remoteDatasource: RemoteDatasource! | |
override func setUpWithError() throws { | |
let urlSession = URLSession(configuration: .mockDefault) | |
remoteDatasource = RemoteDatasource(urlSession: urlSession) | |
} | |
override func tearDownWithError() throws { | |
remoteDatasource = nil | |
MockURLProtocol.data = nil | |
MockURLProtocol.error = nil | |
MockURLProtocol.urlResponse = nil | |
} | |
func testMakeRequest_Validates_InvalidURLs() throws { | |
let expectation = expectation(description: "MakeRequest .invalidURL expectation") | |
remoteDatasource.makeRequest( | |
responseType: EmptyTestModel.self, | |
requestMethod: .get, | |
remotePath: .emptyPath, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success: | |
XCTFail("Shouldn't have succeeded") | |
case .failure(let error): | |
XCTAssertEqual(error, .invalidURL, "makeRequest should return '.invalidURL' when RemotePath points to an invalid resource") | |
expectation.fulfill() | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
func testMakeRequest_Failure() { | |
let expectation = expectation(description: "MakeRequest failure expectation") | |
let expectedError = RemoteDatasourceError.requestFailure(reason: "Bad Network") | |
MockURLProtocol.error = expectedError | |
remoteDatasource.makeRequest( | |
responseType: EmptyTestModel.self, | |
requestMethod: .get, | |
remotePath: .getItems, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success: | |
XCTFail("Shouldn't have succeeded") | |
case .failure(let error): | |
XCTAssertNotNil(error, "makeRequest should return '.requestFailure'") | |
XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription, "makeRequest should return consistent error message") | |
expectation.fulfill() | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
func testMakeRequest_DecodingError() { | |
let expectation = expectation(description: "makeRequest decoding expectation") | |
let jsonData = "{\"status\":200, \"message\":\"Invalid JSON\"}".data(using: .utf8) | |
MockURLProtocol.data = jsonData | |
remoteDatasource.makeRequest( | |
responseType: SuccessTestModel.self, | |
requestMethod: .get, | |
remotePath: .getItems, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success: | |
XCTFail("Shouldn't have succeeded") | |
case .failure(let error): | |
XCTAssertNotNil(error, "there should be an error") | |
expectation.fulfill() | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
func testMakeRequest_Success() { | |
let expectation = expectation(description: "makeRequest expectation") | |
let jsonData = "{\"success\":true}".data(using: .utf8) | |
MockURLProtocol.data = jsonData | |
remoteDatasource.makeRequest( | |
responseType: SuccessTestModel.self, | |
requestMethod: .get, | |
remotePath: .getItems, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success(let response): | |
XCTAssertTrue(response.success, "'response.success' should be true") | |
expectation.fulfill() | |
case .failure: | |
XCTFail("Shouldn't have failed") | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
func testMakeRequest_ResourceNotFound() { | |
let expectation = expectation(description: "makeRequest expectation") | |
let urlResponse = HTTPURLResponse( | |
url: .google1, | |
statusCode: 400, | |
httpVersion: nil, | |
headerFields: nil | |
) | |
MockURLProtocol.urlResponse = urlResponse | |
remoteDatasource.makeRequest( | |
responseType: SuccessTestModel.self, | |
requestMethod: .get, | |
remotePath: .getItems, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success: | |
XCTFail("Shouldn't have succeeded") | |
case .failure(let error): | |
XCTAssertEqual(error, .resourceNotFound, "error should be '.resourceNotFound'") | |
expectation.fulfill() | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
func testMakeRequest_ServerFailure() { | |
let expectation = expectation(description: "makeRequest expectation") | |
let urlResponse = HTTPURLResponse( | |
url: .google1, | |
statusCode: 500, | |
httpVersion: nil, | |
headerFields: nil | |
) | |
MockURLProtocol.urlResponse = urlResponse | |
remoteDatasource.makeRequest( | |
responseType: SuccessTestModel.self, | |
requestMethod: .get, | |
remotePath: .getItems, | |
parameters: [:] | |
) { result in | |
switch result { | |
case .success: | |
XCTFail("Shouldn't have succeeded") | |
case .failure(let error): | |
XCTAssertEqual(error, .serverFailure, "error should be '.serverFailure'") | |
expectation.fulfill() | |
} | |
} | |
wait(for: [expectation], timeout: 1.0) | |
} | |
} |
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
enum RemoteHttpMethod: String { | |
case `get` = "GET" | |
case post = "POST" | |
case put = "PUT" | |
case delete = "DELETE" | |
} |
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
enum RemotePath { | |
case getItems | |
case emptyPath | |
var url: String { | |
let baseURL = "https://www.glovo.com" | |
switch self { | |
case .getItems: | |
return "\(baseURL)/get-items" | |
case .emptyPath: | |
return "" | |
} | |
} | |
} |
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
struct EmptyTestModel: Codable {} | |
struct SuccessTestModel: Codable { | |
let success: Bool | |
} |
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
extension URLSessionConfiguration { | |
static var mockDefault: URLSessionConfiguration { | |
let config = URLSessionConfiguration.ephemeral | |
config.protocolClasses = [MockURLProtocol.self] + (config.protocolClasses ?? []) | |
return config | |
} | |
} | |
extension URL { | |
static var google1 = URL(string: "https://www.google1.com")! | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment