Swift异步函数单元测试报错:Task创建于已失效会话
问题分析与解决方案
核心错误原因
报错Thread 1: "Task created in a session that has been invalidated"的根源在于代码存在两个关键缺陷:
computePrice函数存在未调用completion的分支:JSON解码失败的catch块中,仅更新了showingInvalidAlert但未触发completion,导致测试中的expectation永远无法完成。测试超时后,XCTest框架清理资源时会间接引发会话无效的错误。- 断言可能在非主线程执行:网络错误分支里的
completion(nil)直接在后台线程调用,而XCTest的断言必须在主线程执行,这会导致测试不稳定甚至触发异常。
分步解决方案
1. 修复ViewModel的completion调用路径
确保所有分支都调用completion,避免expectation永久等待:
func computePrice(_ fromLat: Double, _ fromLon: Double,_ toLat: Double, _ toLon: Double, completion: @escaping (Double?) -> Void){ let url = URL(string:"NotShowingMyUrlToStackOverflowButPretendItsHere")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") print("fromLat: \(fromLat) fromLon: \(fromLon) toLat: \(toLat) toLon: \(toLon)") let body = [ "from": [ "latitude": fromLat, "longitude": fromLon ], "to": [ "latitude": toLat, "longitude": toLon ] ] let jsonData = try? JSONSerialization.data(withJSONObject: body) request.httpBody = jsonData URLSession.shared.dataTask(with: request){ data, response, error in guard let data = data, error == nil, (response as? HTTPURLResponse)?.statusCode == 200 else{ // 统一切换到主线程调用completion DispatchQueue.main.async { completion(nil) } return } //Decode JSON response do { let decoder = JSONDecoder() let response1 = try decoder.decode(PriceResponse.self, from: data) DispatchQueue.main.async { completion(response1.price) self.screenDisabled = false } } catch { DispatchQueue.main.async { self.showingInvalidAlert = true // 新增:解码失败时调用completion completion(nil) } print("THE ERROR IS \(error.localizedDescription)") } }.resume() }
2. 确保测试断言在主线程执行
修改测试代码,将断言放在主线程中执行,避免非主线程操作XCTest API:
func testRswPrice(){ //Given (Arrange) let fromLat = 26.336502 let fromLon = -81.439354 let toLat = 26.533842 let toLon = -81.759150 let vm = LocationSearchViewModel() //When (Act) let expectation = self.expectation(description: "Compute Price") vm.computePrice(fromLat, fromLon, toLat, toLon){ value in DispatchQueue.main.async { expectation.fulfill() XCTAssertEqual(value, 8000.0) } } // Wait for the expectation to be fulfilled, or time out after a specified duration wait(for: [expectation], timeout: 20.0) }
3. 检查测试目标的网络配置(可选)
若API使用HTTP协议,需确保测试目标的Info.plist配置了NSAppTransportSecurity以允许非HTTPS请求:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
注:生产环境建议使用HTTPS,此配置仅用于测试或临时场景。
4. 注入URLSession提升测试可控性(进阶)
为避免依赖真实网络,可在ViewModel中注入URLSession,测试时使用Mock Session:
// 修改ViewModel初始化方法 class LocationSearchViewModel { private let urlSession: URLSession init(urlSession: URLSession = .shared) { self.urlSession = urlSession } func computePrice(_ fromLat: Double, _ fromLon: Double,_ toLat: Double, _ toLon: Double, completion: @escaping (Double?) -> Void){ // 替换URLSession.shared为self.urlSession self.urlSession.dataTask(with: request){ data, response, error in // 回调逻辑不变 }.resume() } } // 测试时使用自定义Mock URLSession(需自行实现Mock逻辑) func testRswPrice(){ let mockSession = URLSessionMock() let vm = LocationSearchViewModel(urlSession: mockSession) // 模拟响应数据并执行测试 }
验证方法
修复后重新运行测试:
- 若API返回正常,断言会验证价格是否符合预期
- 若出现网络或解码错误,
completion会被正常调用,expectation不会永久等待,避免会话无效的错误
内容的提问来源于stack exchange,提问作者John Citrowske
相关产品推荐
相关产品推荐

