Thanks to your suggestions regarding MockURLProtocol and Actor, I’ve managed to build a test environment in Xcode 16 using the new Swift 6 concurrency features. I’ve adapted the setup slightly to fit my needs, introducing a HandlerType enum in our actor RequestHandlerStorage to handle success and failure cases separately.
Here’s the tricky part: when I run the tests individually, they all pass. However, running them together results in a failure. Specifically, a test expected an error to be thrown (e.g., when adding a URLResponse instead of an HTTPURLResponse), but instead, the test passes with success unexpectedly. The output is:
Expectation failed: an error was expected but none was thrown. Instead, the result was (12 bytes, <NSHTTPURLResponse: 0x6000032fde60> { URL: http://any-url.com/ } { Status Code: 200, Headers ...
This issue doesn’t occur when running the tests separately.
Tests:
import Testing
import CodingFeed
@Suite("HTTP Client Session")
struct URLSessionHTTPClientTests {
@Test("Get from URL performs a GET request with URL", arguments:
[TestHelpers.Network.anyURL()],
[TestHelpers.Network.makeItemJSON([])]
)
func test_perforsGETRequestWithURL(url: URL, data: Data) async throws {
let sut = makeSUT()
await MockURLProtocol.setSuccessHandler { request in
#expect(request.url == url)
#expect(request.httpMethod == "GET")
return (TestHelpers.Network.makeResponse(for: url), data)
}
_ = try? await sut.get(from: url)
}
@Test("Get from URL fails when response is not HTTP response", arguments:
[TestHelpers.Network.anyURL()],
[TestHelpers.Network.makeItemJSON([])]
)
func test_getFromURL_failsOnNonHTTPResponse(url: URL, data: Data) async throws {
let sut = makeSUT()
await MockURLProtocol.setFailureHandler { _ in
let nonHTTPResponse = URLResponse(
url: url,
mimeType: nil,
expectedContentLength: 0,
textEncodingName: nil
)
return (nonHTTPResponse, data)
}
await #expect(throws: (any Error).self) {
try await sut.get(from: url)
}
}
@Test("Get from URL returns data and HTTP response", arguments:
[TestHelpers.Network.anyURL()],
[TestHelpers.Network.makeItemJSON([])]
)
func test_getFromURL_returnsDataAndHTTPResponse(url: URL, data: Data) async throws {
let sut = makeSUT()
await MockURLProtocol.setSuccessHandler { _ in
return (TestHelpers.Network.makeResponse(for: url), data)
}
let (receivedData, receivedResponse) = try await sut.get(from: url)
#expect(receivedData == data)
#expect(receivedResponse.url == TestHelpers.Network.makeResponse(for: url).url)
#expect(receivedResponse.statusCode == TestHelpers.Network.makeResponse(for: url).statusCode)
}
// MARK: Helpers
private func makeSUT() -> HTTPClient {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: configuration)
return URLSessionHTTPClient(session: session)
}
private actor RequestHandlerStorage {
enum HandlerType {
case success((@Sendable (URLRequest) async throws -> (HTTPURLResponse, Data)))
case failure((@Sendable (URLRequest) async throws -> (URLResponse, Data)))
case none
}
private var currentHandler: HandlerType = .none
func setSuccessHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (HTTPURLResponse, Data)) async {
currentHandler = .success(handler)
}
func setFailureHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (URLResponse, Data)) async {
currentHandler = .failure(handler)
}
func executeHandler(for request: URLRequest) async throws -> (URLResponse, Data) {
switch currentHandler {
case .success(let handler):
return try await handler(request)
case .failure(let handler):
return try await handler(request)
case .none:
throw TestHelpers.Network.anyError()
}
}
}
private final class MockURLProtocol: URLProtocol, @unchecked Sendable {
private static let storage = RequestHandlerStorage()
static func setSuccessHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (HTTPURLResponse, Data)) async {
await storage.setSuccessHandler(handler)
}
static func setFailureHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (URLResponse, Data)) async {
await storage.setFailureHandler(handler)
}
func executeHandler(for request: URLRequest) async throws -> (URLResponse, Data) {
return try await Self.storage.executeHandler(for: request)
}
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
Task {
do {
let (response, data) = try await self.executeHandler(for: request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
}
override func stopLoading() {}
}
}
and here is what we are actually testing:
import Foundation
public protocol HTTPClient {
func get(from url: URL) async throws -> (Data, HTTPURLResponse)
}
public class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
private struct UnexpectedValueRepresentation: Error {}
public func get(from url: URL) async throws -> (Data, HTTPURLResponse) {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw UnexpectedValueRepresentation()
}
return (data, httpResponse)
}
}
I’m working on implementing a modular approach while keeping SOLID principles in mind. However, I’m still getting familiar with Modern Concurrency in Swift.
Here are the steps I’ve tried so far:
• Clearing RequestHandlerStorage before and after each test – no success.
• Using a defer block to ensure cleanup at the end of a function’s scope, but I dropped this approach as defer blocks cannot contain async calls in Swift – no success.
• Registering and unregistering MockURLProtocol before and after each test – no success.
Despite these efforts, the issue persists. Does anyone have suggestions or insights on how to resolve this?
Thank you in advance for your help!