Created
March 19, 2024 17:58
-
-
Save tobitech/03e70a9f6e7c7c9f71caa84e6ff2d710 to your computer and use it in GitHub Desktop.
Mocking URLSession with URLProtocol
This file contains hidden or 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 Foundation | |
import XCTest | |
// 1. mocking url session with URLProtocol | |
// this approach allows us to intercept the network request. | |
// Steps: | |
// i. subclass URLProtocol | |
// ii. implement these methods from the prototol canInit, canonicalRequest, startLoading, stopLoading. | |
// iii. add implementation to startLoading based on a requestHandler closure. | |
// iv. send received response to the client: URLProtocolClient | |
class MockURLProtocol: URLProtocol { | |
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() { | |
guard let handler = MockURLProtocol.requestHandler else { | |
fatalError("Handler is unavailable.") | |
} | |
do { | |
let (response, data) = try handler(request) | |
// send response to didReceive | |
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) | |
// send data to didLoad if available. | |
if let data = data { | |
client?.urlProtocol(self, didLoad: data) | |
} | |
// call didFinishLoading | |
client?.urlProtocolDidFinishLoading(self) | |
} catch { | |
// send error to didFailWithError | |
client?.urlProtocol(self, didFailWithError: error) | |
} | |
} | |
override func stopLoading() {} | |
} | |
enum APIResponseError: Error { | |
case network | |
case parsing | |
case request | |
} | |
// Model | |
struct Post: Decodable { | |
let userId: Int | |
let id: Int | |
let title: String | |
let body: String | |
} | |
// Network Class | |
class PostDetailAPI { | |
let urlSession: URLSession | |
init(urlSession: URLSession = URLSession.shared) { | |
self.urlSession = urlSession | |
} | |
func fetchPostDetail(completion: @escaping (_ result: Result<Post, Error>) -> Void) { | |
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/42")! | |
let dataTask = urlSession.dataTask(with: url) { (data, urlResponse, error) in | |
do { | |
// Check if any error occured. | |
if let error = error { | |
throw error | |
} | |
// Check response code. | |
guard let httpResponse = urlResponse as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { | |
completion(Result.failure(APIResponseError.network)) | |
return | |
} | |
// Parse data | |
if let responseData = data, let object = try? JSONDecoder().decode(Post.self, from: responseData) { | |
completion(Result.success(object)) | |
} else { | |
throw APIResponseError.parsing | |
} | |
} catch { | |
completion(Result.failure(error)) | |
} | |
} | |
dataTask.resume() | |
} | |
} | |
// Unit Testing | |
class PostAPITests: XCTestCase { | |
var postDetailAPI: PostDetailAPI? | |
var expectation: XCTestExpectation! | |
let apiURL = URL(string: "https://jsonplaceholder.typicode.com/posts/42")! | |
override func setUp() { | |
let configuration = URLSessionConfiguration() | |
configuration.protocolClasses = [MockURLProtocol.self] | |
let urlSession = URLSession(configuration: configuration) | |
postDetailAPI = PostDetailAPI(urlSession: urlSession) | |
expectation = expectation(description: "Expectation") | |
} | |
func testSuccessfulResponse() { | |
// Prepare mock response. | |
let userID = 5 | |
let id = 42 | |
let title = "URLProtocol Post" | |
let body = "Post body...." | |
let jsonString = """ | |
{ | |
"userId": \(userID), | |
"id": \(id), | |
"title": "\(title)", | |
"body": "\(body)" | |
} | |
""" | |
let data = jsonString.data(using: .utf8) | |
MockURLProtocol.requestHandler = { request in | |
guard let url = request.url else { throw APIResponseError.request } | |
let response = HTTPURLResponse(url: self.apiURL, statusCode: 200, httpVersion: nil, headerFields: nil)! | |
return (response, data) | |
} | |
postDetailAPI?.fetchPostDetail(completion: { result in | |
switch result { | |
case let .success(post): | |
print(post.id) | |
case let .failure(error): | |
print(error.localizedDescription) | |
} | |
self.expectation?.fulfill() | |
}) | |
wait(for: [self.expectation], timeout: 1.0) | |
} | |
} | |
// To test in Playground | |
class TestObserver: NSObject, XCTestObservation { | |
func testCase(_ testCase: XCTestCase, | |
didFailWithDescription description: String, | |
inFile filePath: String?, | |
atLine lineNumber: Int) { | |
assertionFailure(description, line: UInt(lineNumber)) | |
} | |
} | |
let testObserver = TestObserver() | |
XCTestObservationCenter.shared.addTestObserver(testObserver) | |
PostAPITests.defaultTestSuite.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment