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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:49:30