Skip to content

Instantly share code, notes, and snippets.

@IniongunIsaac
Created August 25, 2023 10:04
Show Gist options
  • Save IniongunIsaac/ebc22e20783ba5cd3853bf68fee5b553 to your computer and use it in GitHub Desktop.
Save IniongunIsaac/ebc22e20783ba5cd3853bf68fee5b553 to your computer and use it in GitHub Desktop.
Simple Networking Setup with XCTests
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() {}
}
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()
}
}
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."
}
}
}
protocol RemoteDatasourceProtocol {
func makeRequest<T: Codable>(
responseType: T.Type,
requestMethod: RemoteHttpMethod,
remotePath: RemotePath,
parameters: [String : Any],
completion: @escaping (Result<T, RemoteDatasourceError>) -> Void
)
}
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)
}
}
enum RemoteHttpMethod: String {
case `get` = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
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 ""
}
}
}
struct EmptyTestModel: Codable {}
struct SuccessTestModel: Codable {
let success: Bool
}
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