SwiftUI Combine - 如何测试等待发布者的异步结果

SwiftUI Combine - How to test waiting for a publisher's async result

我正在侦听发布者的更改,然后在我的管道中异步获取一些数据并使用结果更新视图。但是,我不确定如何使它可测试。我怎样才能最好地等到达到预期?

查看

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.results, id: \.self) {
                Text([=10=])
            }
            .searchable(text: $viewModel.searchText)
        }
    }
}

视图模型

final class ContentViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var results: [String] = []
    private var cancellables = Set<AnyCancellable>()

    init() {
        observeSearchText()
    }

    func observeSearchText() {
        $searchText
            .dropFirst()
            .debounce(for: 0.8, scheduler: DispatchQueue.main)
            .sink { _ in
                Task {
                    await self.fetchResults()
                }
            }.store(in: &cancellables)
    }

    private func fetchResults() async {
        do {
            try await Task.sleep(nanoseconds: 1_000_000_000)
            self.results = ["01", "02", "03"]
        } catch {
            // 
        }
    }
}

测试

class ContentViewTests: XCTestCase {
    func testExample() {
        // Given
        let viewModel = ContentViewModel()

        // When
        viewModel.searchText = "123"

        // Then (FAILS - Not waiting properly for result/update)
        XCTAssertEqual(viewModel.results, ["01", "02", "03"])
    }
}

当前解决方法

如果我使 fetchResults() 可用,我可以 async/await 这适用于我的单元和快照测试,但我担心:

  1. 如果不被外部调用,暴露是不好的做法吗?
  2. 我没有测试我的发布管道
func testExample_Workaround() async {
    // Given
    let viewModel = ContentViewModel()

    // When
    await viewModel.fetchResults()

    // Then
    XCTAssertEqual(viewModel.results, ["01", "02", "03"])
}

您需要通过期望异步等待并通过发布者检查结果。

这是可能的方法。使用 Xcode 13.2 / iOS 15.2

测试
    private var cancelables = Set<AnyCancellable>()
    func testContentViewModel() {
        // Given
        let viewModel = ContentViewModel()

        let expect = expectation(description: "results")
        viewModel.$results
            .dropFirst()     // << skip initial value !!
            .sink {
                XCTAssertEqual([=10=], ["01", "02", "03"])
                expect.fulfill()
            }
            .store(in: &cancelables)

        viewModel.searchText = "123"
        wait(for: [expect], timeout: 3)
    }