You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用URLProtocol子类测试async/await URLSession调用时崩溃问题

解决URLProtocol子类适配async/await URLSession请求时的崩溃问题

问题背景

之前用Rob Napier的“Mockery of protocols”方案做网络请求测试,现在尝试改用继承URLProtocol子类的方式存根网络请求,但遇到了兼容性问题:基于闭包回调的URLSession测试用例能正常通过,但async/await风格的调用会触发EXC_BREAKPOINT崩溃(错误码:code=1, subcode=0x1a587a0f0)。同时也想确认Transport同步存根方案在async/await环境下是否可行。

崩溃原因

本质是传统URLProtocol的实现逻辑没适配async/await的任务生命周期。闭包回调模式下,URLSession靠客户端闭包接收结果后自行结束任务;但async/await的URLSession API依赖URLProtocolClient的完整信号流转——如果URLProtocol子类没正确调用urlProtocolDidFinishLoading(_:)(成功场景)或urlProtocol(_:didFailWithError:)(失败场景),async/await的Task会一直处于挂起状态,最终触发底层断点崩溃。

修复方案:适配async/await的URLProtocol子类实现

核心修改点

确保startLoading()方法中,无论成功或失败,都完整调用URLProtocolClient的回调方法,给async/await任务明确的结束信号。

示例实现:

class MockURLProtocol: URLProtocol {
    static var mockMap: [URL: (data: Data?, response: URLResponse?, error: Error?)] = [:]

    override class func canInit(with request: URLRequest) -> Bool {
        return mockMap.keys.contains(request.url!)
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        guard let url = request.url, let mock = MockURLProtocol.mockMap[url] else {
            client?.urlProtocol(self, didFailWithError: NSError(domain: "MockProtocol", code: -1))
            return
        }

        // 模拟错误场景
        if let error = mock.error {
            client?.urlProtocol(self, didFailWithError: error)
            return
        }

        // 发送响应头
        if let response = mock.response {
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        }

        // 发送响应数据
        if let data = mock.data {
            client?.urlProtocol(self, didLoad: data)
        }

        // 必须调用:标记任务完成,否则async/await任务会挂起崩溃
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {}
}

配套测试配置

测试时必须使用注册了自定义URLProtocol的URLSession实例,不能用默认的URLSession.shared:

func testAsyncAwaitRequest() async throws {
    // 1. 配置自定义Session
    let config = URLSessionConfiguration.ephemeral
    config.protocolClasses = [MockURLProtocol.self]
    let session = URLSession(configuration: config)

    // 2. 设置存根数据
    let testURL = URL(string: "https://test.example.com")!
    MockURLProtocol.mockMap[testURL] = (
        data: "async test".data(using: .utf8),
        response: HTTPURLResponse(url: testURL, statusCode: 200, httpVersion: nil, headerFields: nil),
        error: nil
    )

    // 3. 执行async/await请求并断言
    let (data, response) = try await session.data(from: testURL)
    XCTAssertEqual(String(data: data, encoding: .utf8), "async test")
    XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200)

    // 清理存根
    MockURLProtocol.mockMap.removeValue(forKey: testURL)
}

Transport方案在async/await环境下的可行性

完全可行,甚至比URLProtocol方案更适配async/await。Transport方案通过抽象网络层协议,彻底解耦业务代码与URLSession,测试时直接替换为Mock实现,无需依赖系统的URLProtocol机制,也不需要XCTestExpectation。

示例实现

  1. 定义支持async/await的Transport协议:
protocol NetworkTransport {
    func data(from url: URL) async throws -> (Data, URLResponse)
    // 可扩展其他网络方法,如upload、download等
}
  1. 真实网络实现:
class URLSessionTransport: NetworkTransport {
    private let session: URLSession

    init(session: URLSession = .shared) {
        self.session = session
    }

    func data(from url: URL) async throws -> (Data, URLResponse) {
        try await session.data(from: url)
    }
}
  1. Mock测试实现:
class MockTransport: NetworkTransport {
    var stubbedData: Data?
    var stubbedResponse: URLResponse?
    var stubbedError: Error?

    func data(from url: URL) async throws -> (Data, URLResponse) {
        if let error = stubbedError {
            throw error
        }
        guard let data = stubbedData, let response = stubbedResponse else {
            throw NSError(domain: "MockTransport", code: -1)
        }
        return (data, response)
    }
}
  1. 测试用例:
func testTransportAsyncAwait() async throws {
    let mockTransport = MockTransport()
    let testURL = URL(string: "https://test.example.com")!
    
    // 设置存根
    mockTransport.stubbedData = "transport test".data(using: .utf8)
    mockTransport.stubbedResponse = HTTPURLResponse(url: testURL, statusCode: 200, httpVersion: nil, headerFields: nil)

    // 执行请求并断言
    let (data, response) = try await mockTransport.data(from: testURL)
    XCTAssertEqual(String(data: data, encoding: .utf8), "transport test")
    XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200)
}

这种方案的优势在于:测试代码更简洁,避免了URLProtocol的全局状态管理问题,同时完全适配async/await的结构化并发特性。

内容的提问来源于stack exchange,提问作者Chris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 18:55:45