API单元测试报错:[UIApplication applicationState]必须仅在主线程使用
[UIApplication applicationState]必须在主线程调用的错误 嗨,我来帮你搞定这个问题!你遇到的这个报错,本质是因为单元测试中有代码在非主线程访问了UIKit的API——具体来说,应该是ServiceHelper里的showSpinner: true触发了和UIApplication applicationState相关的检查,而UIKit的所有操作都必须在主线程执行,单元测试环境下这个检查会更严格,直接导致测试崩溃。
问题根源拆解
你的registerApi方法调用sendRequest时开启了showSpinner: true,而ServiceHelper内部大概率在后台线程处理完请求后,直接去更新UI(比如隐藏加载框),或者在判断是否显示spinner时访问了applicationState,这就踩了UIKit必须在主线程操作的红线。另外,你的测试闭包也可能没在主线程处理断言,这也会埋下隐患。
一步步解决
1. 先给测试“减负”:禁用UI相关操作
单元测试本来就不该管UI展示,所以我们可以先让测试时不显示spinner,或者直接mock掉网络请求,避免触发UI逻辑:
修改你的测试函数,两种方案选一个:
func testApiWorking() { let controller = WorldCupChooseCountryVC() let expected = XCTestExpectation(description: "API请求成功完成") // 方案一:如果能改registerApi的参数,加个是否显示spinner的开关 // controller.registerApi(path: "get-country", player_id: 163, contest_id: 1, country_id: 1, showSpinner: false) { success in // 方案二:直接替换成Mock的ServiceHelper,跳过真实网络请求 ServiceHelper.sharedInstance = MockServiceHelper() controller.registerApi(path: "get-country", player_id: 163, contest_id: 1, country_id: 1) { success in // 务必在主线程里处理测试断言,避免线程问题 DispatchQueue.main.async { if success { expected.fulfill() } else { XCTFail("API请求意外失败") } } } // 把超时时间设长一点,避免网络慢导致误判 waitForExpectations(timeout: 5.0) { error in if let error = error { XCTFail("测试超时:\(error.localizedDescription)") } } }
2. 根治问题:修复ServiceHelper的线程逻辑
不管是不是测试环境,你的ServiceHelper都应该保证UI相关操作在主线程执行。修改sendRequest的完成回调部分:
func sendRequest(path: String, params: [String: Any], showSpinner: Bool, completionHandler: @escaping (Any?, Error?) -> Void) { // ... 原来的网络请求逻辑 ... // 请求完成后,把UI操作和回调都切到主线程 DispatchQueue.main.async { if showSpinner { // 这里处理隐藏spinner的逻辑,现在是安全的主线程 } completionHandler(response, error) } }
3. 优化可测试性:用依赖注入替代单例
单例虽然好用,但会让单元测试很难mock。我们可以用协议+依赖注入的方式,让代码更灵活:
首先定义一个协议,让ServiceHelper遵守:
protocol ServiceHelperProtocol { func sendRequest(path: String, params: [String: Any], showSpinner: Bool, completionHandler: @escaping (Any?, Error?) -> Void) } // 让原有的ServiceHelper遵守这个协议 extension ServiceHelper: ServiceHelperProtocol {}
然后在VC里把单例换成可注入的属性:
class WorldCupChooseCountryVC: UIViewController { // 默认用单例,测试时可以替换成mock var serviceHelper: ServiceHelperProtocol = ServiceHelper.sharedInstance func registerApi(path: String, player_id: Int, contest_id: Int, country_id: Int, success: () -> Void) { serviceHelper.sendRequest(path: "register-country", params: ["api_token": Constants.USER_INFO["api_token"].rawValue, "player_id": player_id, "country_id": country_id, "contest_id": contest_id], showSpinner: true, completionHandler: { (response, error) in if error != nil { // 错误处理 } else { success() } }) } }
最后写个Mock类用于测试:
class MockServiceHelper: ServiceHelperProtocol { func sendRequest(path: String, params: [String: Any], showSpinner: Bool, completionHandler: @escaping (Any?, Error?) -> Void) { // 模拟成功响应,直接调用回调 completionHandler(nil, nil) } } // 测试函数就可以这么写 func testApiWorking() { let controller = WorldCupChooseCountryVC() // 注入mock对象 controller.serviceHelper = MockServiceHelper() let expected = XCTestExpectation(description: "API请求成功完成") controller.registerApi(path: "get-country", player_id: 163, contest_id: 1, country_id: 1) { expected.fulfill() } waitForExpectations(timeout: 1.0) { error in if let error = error { XCTFail("测试超时:\(error.localizedDescription)") } } }
最后总结
最快速的修复是把ServiceHelper里的UI操作和回调都切到主线程;如果想让测试更稳定、更高效,推荐用依赖注入+mock的方式,彻底摆脱对真实网络和UI环境的依赖。
内容的提问来源于stack exchange,提问作者Rob

