Swift测试ObservableObject:异步操作完成前断言触发问题求助
解决ObservableObject单元测试中断言提前触发的问题
问题描述
我在测试一个ObservableObject,已通过依赖注入模拟了其网络请求。问题在于,尽管我await了异步函数,但单元测试中的断言会在Published属性设置前触发。
ViewModel流程为:调用fetchWhatsNew函数,解析数据并验证showMarketing为true且URL有效,若满足则将shouldShowMarketingPage设为true,供SwiftUI视图调用并展示URL。
ViewModel代码
final class MarketingViewModel: ObservableObject { /// Decides if we need to show the marketing page to the user @Published private(set) var shouldShowMarketingPage: Bool = false @Published private(set) var whatNewUrl: String? /// bool value from the api response private var showMarketingFromApi = false private(set) var error: Error? private let networkingManager: NetworkingManagerImplementation private let marketingRowId: String = "i-3JdpsB-rxH" init(netwokringManager: NetworkingManagerImplementation = NetworkingManger.shared) { self.networkingManager = netwokringManager } func fetchWhatsNew() async { do { let res = try await networkingManager.request(session: .shared, .getCodaApiData(documentId: .AppDoc, tableId: .marketingTableId), type: ShowMarketingInApp.self) handleResponse(response: res) } catch { print("whats new error: ", error.localizedDescription) self.error = error } } private func handleResponse(response: ShowMarketingInApp) { if let marketingRow = response.items.first(where: { $0.id == marketingRowId }) { DispatchQueue.main.async { self.showMarketingFromApi = marketingRow.values.showMarketing self.whatNewUrl = marketingRow.values.marketingUrl self.checkShouldShowMarketingPage() } } } private func checkShouldShowMarketingPage() { guard showMarketingFromApi, isValidURL(whatNewUrl) else { self.shouldShowMarketingPage = false return } self.shouldShowMarketingPage = true } private func isValidURL(_ urlString: String?) -> Bool { guard let urlString = urlString, let _ = URL(string: urlString) else { return false } return true } }
测试类代码
class MarketingVmSuccessTests: XCTestCase { private var networkMock: NetworkingManagerImplementation! private var marketingVM: MarketingViewModel! override func setUp() async throws { try await super.setUp() } override func tearDown() async throws { networkMock = nil marketingVM = nil try await super.tearDown() } private func createViewModel(with successType: MarketingMockSuccessType) { networkMock = MarketingNetworkManagerSuccessMock(successType: successType) marketingVM = MarketingViewModel(netwokringManager: networkMock) } func test_FetchWhatsNew_Success_showMarketing_hasUrl() async { createViewModel(with: .showMarketing_hasUrl) await marketingVM.fetchWhatsNew() XCTAssertTrue(marketingVM.shouldShowMarketingPage, "show marketing page should be true") XCTAssertEqual(marketingVM.whatNewUrl, "https://myurl.com", "Urls dont match") } }
解决方案
一、优化ViewModel(推荐方案)
问题核心是handleResponse中使用了DispatchQueue.main.async,该异步任务会在await fetchWhatsNew()执行完毕后才调度到主线程,导致断言提前触发——因为await仅等待networkingManager.request和handleResponse的同步代码完成,不会等待主线程的异步闭包执行。
修改步骤:
- 将
DispatchQueue.main.async替换为await MainActor.run,并让handleResponse变为异步函数,确保await fetchWhatsNew()会等待主线程上的属性更新完成。
修改后的代码:
// 修改fetchWhatsNew,加上await调用handleResponse func fetchWhatsNew() async { do { let res = try await networkingManager.request(session: .shared, .getCodaApiData(documentId: .AppDoc, tableId: .marketingTableId), type: ShowMarketingInApp.self) await handleResponse(response: res) } catch { print("whats new error: ", error.localizedDescription) self.error = error // 若需更新UI相关错误属性,也建议用MainActor.run await MainActor.run { // 比如更新UI用的错误状态 } } } // 修改handleResponse为异步函数,用MainActor.run替代DispatchQueue.main.async private func handleResponse(response: ShowMarketingInApp) async { if let marketingRow = response.items.first(where: { $0.id == marketingRowId }) { await MainActor.run { self.showMarketingFromApi = marketingRow.values.showMarketing self.whatNewUrl = marketingRow.values.marketingUrl self.checkShouldShowMarketingPage() } } }
二、优化测试代码(不改动ViewModel的方案)
如果不想调整ViewModel代码,可以通过等待主线程任务完成或监听Published属性变化来确保断言时机正确。
方法1:使用XCTestExpectation监听属性变化
func test_FetchWhatsNew_Success_showMarketing_hasUrl() async { createViewModel(with: .showMarketing_hasUrl) let expectation = XCTestExpectation(description: "Marketing page state updated") // 监听shouldShowMarketingPage的变化,跳过初始false值 let cancellable = marketingVM.$shouldShowMarketingPage .dropFirst() .sink { isShowing in if isShowing { expectation.fulfill() } } await marketingVM.fetchWhatsNew() // 等待期望完成,设置1秒超时 await fulfillment(of: [expectation], timeout: 1.0) XCTAssertTrue(marketingVM.shouldShowMarketingPage, "show marketing page should be true") XCTAssertEqual(marketingVM.whatNewUrl, "https://myurl.com", "Urls dont match") cancellable.cancel() }
方法2:等待主线程任务全部完成
func test_FetchWhatsNew_Success_showMarketing_hasUrl() async { createViewModel(with: .showMarketing_hasUrl) await marketingVM.fetchWhatsNew() // 等待主线程上所有异步任务执行完毕 await MainActor.run {} XCTAssertTrue(marketingVM.shouldShowMarketingPage, "show marketing page should be true") XCTAssertEqual(marketingVM.whatNewUrl, "https://myurl.com", "Urls dont match") }
内容的提问来源于stack exchange,提问作者Trey6
相关产品推荐
相关产品推荐

