Skip to content

Instantly share code, notes, and snippets.

@tobitech
Created March 19, 2024 17:58
Show Gist options
  • Save tobitech/03e70a9f6e7c7c9f71caa84e6ff2d710 to your computer and use it in GitHub Desktop.
Save tobitech/03e70a9f6e7c7c9f71caa84e6ff2d710 to your computer and use it in GitHub Desktop.
Mocking URLSession with URLProtocol
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