Last active
March 14, 2025 21:38
-
-
Save thomsmed/90d34568877918b77dca7880614e84ee to your computer and use it in GitHub Desktop.
A simple MockURLProtocol for Unit/Integration tests relying on URLSession.
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
// | |
// MockURLProtocol.swift | |
// | |
/// Inspired by [Testing Tips & Tricks](https://developer.apple.com/videos/play/wwdc2018/417). | |
final class MockURLProtocol: URLProtocol { | |
static nonisolated(unsafe) var responseBuilders: [(URLRequest) throws -> (Data, HTTPURLResponse, URLRequest?)] = [] | |
override static func canInit(with request: URLRequest) -> Bool { | |
!responseBuilders.isEmpty | |
} | |
override static func canonicalRequest(for request: URLRequest) -> URLRequest { | |
request | |
} | |
override func startLoading() { | |
do { | |
let responseBuilder = Self.responseBuilders.removeFirst() | |
let (data, response, request) = try responseBuilder(request) | |
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) | |
if let request { | |
client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response) | |
} | |
client?.urlProtocol(self, didLoad: data) | |
client?.urlProtocolDidFinishLoading(self) | |
} catch { | |
client?.urlProtocol(self, didFailWithError: error) | |
} | |
} | |
override func stopLoading() {} | |
} | |
// MARK: Snipet of a simple HTTP Client | |
public extension HTTP { | |
private final class DoNotFollowRedirectsDelegate: NSObject, URLSessionTaskDelegate { | |
func urlSession( | |
_ session: URLSession, | |
task: URLSessionTask, | |
willPerformHTTPRedirection response: HTTPURLResponse, | |
newRequest request: URLRequest | |
) async -> URLRequest? { | |
nil | |
} | |
} | |
final class Client: Sendable { | |
// ... | |
private func send(_ request: HTTP.Request) async throws -> SendResult { | |
// ... | |
var (data, httpURLResponse) = try await URLSession.shared.data( | |
for: urlRequest, delegate: request.followRedirects ? nil : DoNotFollowRedirectsDelegate() | |
) | |
// ... | |
} | |
} | |
} | |
// MARK: Usage (with automatic redirect) | |
let configuration = URLSessionConfiguration.ephemeral | |
configuration.protocolClasses = [MockURLProtocol.self] | |
let session = URLSession(configuration: configuration) | |
let httpClient = HTTP.Client(session: session) | |
let redirectingResponse = HTTPURLResponse() | |
let redirectingResponseData = Data("Some Redirecting Body".utf8) | |
let response = HTTPURLResponse() | |
let responseData = Data("Some Body".utf8) | |
MockURLProtocol.responseBuilders = [ | |
{ request in | |
(redirectingResponseData, redirectingResponse, request) | |
}, | |
{ request in | |
(responseData, response, nil) | |
}, | |
] | |
let url = URL(string: "https://example.ios")! | |
let request = HTTP.Request( | |
url: url, | |
method: .post, | |
body: Data(), | |
headers: [ | |
.userAgent("Some User-Agent"), | |
.accept(.json) | |
], | |
followRedirects: true | |
) | |
let httpResponse = try await httpClient.send( | |
request, | |
tags: ["My Tag": "Hello World!"] | |
) | |
#expect(httpResponse.body == responseData) | |
// MARK: Usage (without automatic redirect) | |
let configuration = URLSessionConfiguration.ephemeral | |
configuration.protocolClasses = [MockURLProtocol.self] | |
let session = URLSession(configuration: configuration) | |
let httpClient = HTTP.Client(session: session) | |
let redirectingResponse = HTTPURLResponse() | |
let redirectingResponseData = Data("Some Redirecting Body".utf8) | |
let response = HTTPURLResponse() | |
let responseData = Data("Some Body".utf8) | |
MockURLProtocol.responseBuilders = [ | |
{ request in | |
(redirectingResponseData, redirectingResponse, request) | |
}, | |
{ request in | |
(responseData, response, nil) | |
}, | |
] | |
let url = URL(string: "https://example.ios")! | |
let request = HTTP.Request( | |
url: url, | |
method: .post, | |
body: Data(), | |
headers: [ | |
.userAgent("Some User-Agent"), | |
.accept(.json) | |
], | |
followRedirects: false | |
) | |
let httpResponse = try await httpClient.send( | |
request, | |
tags: ["My Tag": "Hello World!"] | |
) | |
#expect(httpResponse.body == redirectingResponseData) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment