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

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的同步代码完成,不会等待主线程的异步闭包执行。

修改步骤:

  1. 将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 16:28:08