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

URLSession单元测试未触发MockURLProtocol的requestHandler问题

iOS单元测试URLProtocol Mock失效、请求计数异常问题修复

问题涉及的现有实现

  • 自定义MockURLProtocol继承URLProtocol实现网络Mock:
    • 静态属性contactedURLs记录所有触发请求的URL
    • 静态属性callCount统计请求调用次数
    • 静态方法clear()用于重置所有内部状态
    • 静态闭包requestHandler支持自定义返回预设HTTPURLResponse与测试Data
    • 重写canInit(with:)、canonicalRequest(for:)、startLoading()、stopLoading()实现核心Mock逻辑
  • 封装MovieRequest结构体处理电影列表拉取:
    • 持有urlSession属性,初始化方法支持传入自定义URLSession实例,默认参数为URLSession.shared
    • 内部getMovies(completion:)方法通过持有的session调用dataTask(with:completionHandler:)发起请求,依次处理网络错误、HTTP状态码校验、JSON解码逻辑,最终通过completion闭包返回Result<[Movie], MovieError>结果
  • 测试类MovieTaskTests的setUp()配置:
    • #if DEBUG分支下创建URLSessionConfiguration.default实例,将protocolClasses设置为[MockURLProtocol.self],用该配置初始化自定义URLSession
    • #else分支直接使用URLSession.shared
    • 初始化被测ViewController实例sut与MovieRequest实例movieRequest,将配置好的URLSession注入movieRequest

异常现象

  • 运行测试用例test_FetchShouldBeCalledOnceWithThisURL()时,movieRequest始终使用URLSession.shared发起真实网络请求,未触发MockURLProtocol.requestHandler返回预设测试数据,拿到真实接口返回的全量JSON
  • 删除#else分支强制使用自定义配置的URLSession后,请求返回空数组,MockURLProtocol.callCount统计值为3,不符合预期的1次调用,仅MockURLProtocol.contactedURLs的URL校验逻辑可正常通过

根因说明

  1. 条件编译分支失效:XCTest测试Target默认不会继承主Target的DEBUG预处理宏,若未手动给测试Target配置DEBUG=1的预处理宏,#if DEBUG分支不会执行,默认走#else分支的URLSession.shared。
    注意:URLSession.shared是全局单例,系统不支持修改其配置的protocolClasses属性,任何针对shared session的protocol注册操作都不会生效,必须通过URLSession(configuration:)初始化的自定义session才能使用自定义URLProtocol。
  2. 请求计数为3、返回空数组的核心原因是MockURLProtocol的startLoading()实现缺失关键回调:自定义URLProtocol在返回响应数据后,必须调用urlProtocolDidFinishLoading(_:)告知URLSession请求已完成,否则系统会判定请求失败,默认触发2次重试,加上初始请求总次数刚好为3;如果未按顺序回调响应头、响应体方法,上层逻辑拿不到正确数据就会解析出空数组。
  3. 缺少请求去重标记:如果未在canonicalRequest(for:)中给已拦截的请求打标记,同一个请求可能被URLProtocol系统逻辑多次触发canInit判断,导致重复计数。

修复步骤

  • 移除测试配置中的#if DEBUG/#else条件分支,测试Target内固定使用注册了MockURLProtocol的自定义配置初始化URLSession,不要依赖DEBUG宏判断测试环境。如果需要保留条件判断,手动给测试Target添加预处理宏:打开测试Target的Build Settings,搜索Preprocessor Macros,在Debug配置项中添加DEBUG=1。
  • 补全startLoading()的完整回调逻辑,参考实现:
override func startLoading() {
    guard let handler = MockURLProtocol.requestHandler else {
        fatalError("Forgot to set mock request handler in test")
    }
    // 统计计数、记录URL
    Self.callCount += 1
    if let url = request.url {
        Self.contactedURLs.append(url)
    }
    do {
        let (response, data) = try handler(request)
        // 按顺序回调响应头、响应体、完成标记
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        client?.urlProtocol(self, didLoad: data)
        client?.urlProtocolDidFinishLoading(self)
    } catch {
        // 错误场景也要回调错误终止请求,避免重试
        client?.urlProtocol(self, didFailWithError: error)
    }
}
  • 补全请求拦截的去重逻辑,避免重复计数:
override class func canInit(with request: URLRequest) -> Bool {
    // 仅拦截业务相关的接口请求,过滤系统探测、ATS校验等无关请求
    guard let host = request.url?.host, host == "你的业务接口域名" else {
        return false
    }
    // 已经标记处理过的请求不再重复拦截
    return URLProtocol.property(forKey: "MockURLProtocolHandledKey", in: request) == nil
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
    guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
        return request
    }
    // 给当前请求打已处理标记
    URLProtocol.setProperty(true, forKey: "MockURLProtocolHandledKey", in: mutableRequest)
    return mutableRequest.copy() as! URLRequest
}
  • 补充测试生命周期清理逻辑:在tearDown()方法中调用MockURLProtocol.clear()重置静态状态,同时调用自定义session的invalidateAndCancel()方法释放资源,避免不同测试用例之间的状态污染。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 08:51:34