Write an async request function whose transport is protocol-injected so it can be unit-tested with no network
Write an async function that fetches and decodes a Decodable model, with its transport injected through a protocol so a unit test needs no network.
Requirements:
URLSession should be able to conform to it.
- Declare a
Transportprotocol with anasyncmethod returning(Data, URLResponse); - The function throws on a non-2xx status and decodes the body with
JSONDecoder. - No
URLSession.sharedinside the function — the transport is a parameter.
protocol Transport {
// declare the async transport method
}
func fetch<T: Decodable>(_ request: URLRequest, using transport: Transport) async throws -> T {
// your code here
}
Write the implementation.
Define a Transport protocol — func data(for: URLRequest) async throws -> (Data, URLResponse) — and conform URLSession to it. The function builds the request, awaits the transport, checks status, and decodes with JSONDecoder. Production injects the real URLSession; a test injects a stub with canned data, so it runs offline.
- ✗Calling
URLSession.shareddirectly, leaving nothing to stub - ✗Not checking the HTTP status before decoding the body
- ✗Making the function non-generic so each model needs its own copy
- →How does a
URLProtocolsubclass intercept requests without a real server? - →Why is parameter injection easier to test than a singleton transport?
The transport becomes a protocol with one async method that URLSession already satisfies, so a test substitutes a stub with no network:
protocol Transport {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
extension URLSession: Transport {}
func fetch<T: Decodable>(_ request: URLRequest, using transport: Transport) async throws -> T {
let (data, response) = try await transport.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
}
// Test double — no network:
struct StubTransport: Transport {
let result: (Data, URLResponse)
func data(for request: URLRequest) async throws -> (Data, URLResponse) { result }
}
The status check precedes decode, so an error body is never parsed as the model. In a test, pass a StubTransport with canned Data and an HTTPURLResponse — the call is deterministic and offline.