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

ChatScreenViewModel的AsyncStream轮询功能单元测试失败,Mock未正常发射数据求排查

ChatScreenViewModel的AsyncStream轮询功能单元测试失败,Mock未正常发射数据求排查

我帮你排查了一下代码,核心问题出在AsyncStream的初始化逻辑和测试断言的时机上,具体分析和修复方案如下:

1. 根本原因:AsyncStream闭包延迟执行导致Continuation未初始化

你写的MockMessageProvider里,AsyncStream的初始化闭包是只有当流被订阅(也就是开始for await迭代)时才会执行的。测试里你调用sut.onAppear()后立刻调用emit,但此时后台Task可能还没开始遍历流,闭包没执行,continuation还是nil,自然发不出任何数据。

另外,你的MessageProviderUseCase协议里的poll方法标记了async其实是多余的——创建AsyncStream是同步操作,不需要异步执行,这也会导致测试里的异步流程混淆。

2. 修复步骤

第一步:修正Mock和协议定义

先去掉协议和Mock中多余的async,确保continuation能在流创建后被正确初始化:

// 修改协议
protocol MessageProviderUseCase: Sendable {
    func poll(interval: TimeInterval) -> AsyncStream<MessagePollResult> // 移除async
}

// 修改Mock
private class MockMessageProvider: MessageProviderUseCase {
    private var continuation: AsyncStream<MessagePollResult>.Continuation?
    private(set) var pollCallCount: Int = 0
    private(set) var pollIntervalInput: TimeInterval?

    func poll(interval: TimeInterval) -> AsyncStream<MessagePollResult> {
        pollCallCount += 1
        pollIntervalInput = interval
        return AsyncStream<MessagePollResult> { continuation in
            self.continuation = continuation
        }
    }

    func emit(_ value: MessagePollResult) {
        continuation?.yield(value)
    }

    func finish() {
        continuation?.finish()
    }
}

第二步:修正测试的断言时机

原来的defer块会在函数结束时同步执行,但sink的回调是异步的,此时messages数组可能还没接收到更新。改用XCTestExpectation来等待异步事件完成:

@Test func onAppearShouldReturnInitialMessagesAndStartPolling() async throws {
    let mockMessageProvider = MockMessageProvider()
    let sut = createSUT(messageProvider: mockMessageProvider)
    
    // 创建期望对象,等待异步事件
    let initialEmptyExpectation = expectation(description: "Received initial empty messages")
    let polledMessagesExpectation = expectation(description: "Received polled messages")
    
    var messages: [Message] = []
    let cancellable = sut.$messages.sink { incoming in
        messages.append(incoming)
        switch messages.count {
        case 1:
            // 初始空数组
            XCTAssertEqual(incoming.count, 0)
            initialEmptyExpectation.fulfill()
        case 2:
            // 收到轮询返回的消息
            XCTAssertGreaterThan(incoming.count, 0)
            polledMessagesExpectation.fulfill()
        default:
            break
        }
    }
    defer { cancellable.cancel() }
    
    sut.onAppear()
    
    // 先等待初始空数组被发送,确保流已经被订阅,continuation已初始化
    await fulfillment(of: [initialEmptyExpectation], timeout: 1.0)
    
    // 现在再发射测试数据
    mockMessageProvider.emit(MessagePollResult.success(fakeMessages))
    
    // 等待轮询消息被接收
    await fulfillment(of: [polledMessagesExpectation], timeout: 1.0)
    
    // 最后断言poll方法被调用次数
    XCTAssertEqual(mockMessageProvider.pollCallCount, 1)
}

3. 额外说明

如果你因为真实业务逻辑必须保留poll方法的async,那只需要确保在测试中等待流被订阅后再调用emit——上面的initialEmptyExpectation已经帮你做到了这一点,因为只有当流开始迭代,sut.$messages才会收到初始的空数组。

这样修改后,你的测试应该就能正常捕获到Mock发射的数据了。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 03:13:02