Skip to content

Instantly share code, notes, and snippets.

@thomsmed
Last active March 14, 2025 21:38
Show Gist options
  • Save thomsmed/90d34568877918b77dca7880614e84ee to your computer and use it in GitHub Desktop.
Save thomsmed/90d34568877918b77dca7880614e84ee to your computer and use it in GitHub Desktop.
A simple MockURLProtocol for Unit/Integration tests relying on URLSession.
//
// 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