【问题标题】:XCTest testing asyncronous Combine @Publishers [duplicate]XCTest 测试异步合并 @Publishers [重复]
【发布时间】:2021-11-16 04:09:43
【问题描述】:

我正在开发一个 iOS 应用程序(使用 Swift、XCTest 和 Combine),试图在我的视图模型中测试一个函数,该函数在发布者上调用和设置 sink。我想测试视图模型,不是发布者本身。我真的不想使用DispatchQueue.asyncAfter(,因为理论上我不知道发布者需要多长时间才能回复。例如,我将如何测试 XCTAssertFalse(viewModel.isLoading)

class ViewModel: ObservableObject {
  @Published var isLoading: Bool = false
  @Published var didError: Bool = false
  var dataService: DataServiceProtocol

  init(dataService: DataServiceProtocol) {
    self.dataService = dataService
  }

  func getSomeData() { // <=== This is what I'm hoping to test
    isLoading = true
    dataService.getSomeData() //<=== This is the Publisher
      .sink { (completion) in
        switch completion {
          case .failure(_):
            DispatchQueue.main.async {
              self.didError = true
            }
          case .finished:
            print("finished")
         }
                        
       DispatchQueue.main.async {
         self.isLoading = false
       }
     } receiveValue: { (data) in
         print("Ok here is the data", data)
    }
  }
}

我想编写一个如下所示的测试:

func testGetSomeDataDidLoad() {
  // this will test whether or not getSomeData
  // loaded properly

  let mockDataService: DataServiceProtocol = MockDataService
  let viewModel = ViewModel(dataService: mockDataService)

  viewModel.getSomeData()

  // ===== THIS IS THE PROBLEM...how do we know to wait for getSomeData? ======
  // It isn't a publisher...so we can't listen to it per se... is there a better way to solve this?

  XCTAssertFalse(viewModel.isLoading)
  XCTAssertFalse(viewModel.didError)
}

真的希望重构我们当前的测试,这样我们就不会使用DispatchQueue.asyncAfter(

【问题讨论】:

    标签: ios swift testing xctest combine


    【解决方案1】:

    我建议您查看combine-schedulers 包。使用该软件包,您可以更改 ViewModel 以采用 AnySchedulerOf&lt;DispatchQueue&gt; 参数:

    class ViewModel: ObservableObject {
      @Published var isLoading: Bool = false
      @Published var didError: Bool = false
      var dataService: DataServiceProtocol
      var scheduler: AnySchedulerOf<DispatchQueue>
    
      init(
        dataService: DataServiceProtocol,
        scheduler: AnySchedulerOf<DispatchQueue>
      ) {
        self.dataService = dataService
        self.scheduler = scheduler
      }
    
      func getSomeData() {
        isLoading = true
        dataService
          .getSomeData()
    
          // ********
          // We use the scheduler instead of DispatchQueue.main.
          // ********
          .receive(on: scheduler)
    
          .sink { (completion) in
            switch completion {
            case .failure(_):
              self.didError = true
            case .finished:
              print("finished")
            }
            self.isLoading = false
          }
        } receiveValue: { (data) in
             print("Ok here is the data", data)
        }
      }
    }
    

    当您在生产环境中创建ViewModel 时,您传入的是真实的DispatchQueue.main

    let viewModel = ViewModel(
      dataService: LiveDataService(),
      scheduler: DispatchQueue.main.eraseToAnyScheduler()
    )
    

    在您的测试中,您传入TestSchedulerOf&lt;DispatchQueue&gt;,并手动驱动时间流:

    func testGetSomeDataDidLoad() {
      let mockDataService: DataServiceProtocol = MockDataService()
      let clock = DispatchQueue.test
      let viewModel = ViewModel(
        dataService: mockDataService,
        scheduler: clock    
      )
    
      viewModel.getSomeData()
    
      // Time hasn't advanced yet, so the model is still loading:
      XCTAssertTrue(viewModel.isLoading)
      XCTAssertFalse(viewModel.didError)
    
      // Assuming MockDataService completed synchronously, there are actions
      // queued in the test scheduler.
      // Run the test scheduler until there are not pending actions left.
      clock.run()
    
      // Now viewModel should have finished loading.
      XCTAssertFalse(viewModel.isLoading)
      XCTAssertFalse(viewModel.didError)
    }
    

    【讨论】:

      【解决方案2】:

      是的,每个人都在说,MVVM 提高了可测试性。这是非常正确的,因此是推荐的模式。但是,如何在教程中很少显示您如何测试视图模型。那么,我们如何测试这个东西呢?

      测试视图模型的基本思想是使用可以执行以下操作的模拟:

      1. 模拟必须在其输出(即发布的属性)中记录更改
      2. 记录输出的变化
      3. 对输出应用断言函数
      4. 可能会记录更多更改

      为了更好地处理以下测试,请稍微重构您的 ViewModel,以便它使用一个结构获得一个表示您的视图状态的值:

          final class MyViewModel {
              struct ViewState {
                  var isLoading: Bool = false 
                  var didError: Bool = false
              }
      
              @Published private(set) var viewState: ViewState = .init()
      
              ...
          }
      

      然后,为您的视图定义一个 Mock。你可能会尝试这样的事情,这是一个非常幼稚的实现:

      mock view 还会得到一个断言函数列表,这些函数会按顺序测试你的视图状态。

          class MockView {
              var viewModel: MyViewModel
              var cancellable = Set<AnyCancellable>()
      
              typealias AssertFunc = (MyViewModel.ViewState) -> Void
              let asserts: ArraySlice<AssertFunc>
      
              private var next: AssertFunc? = nil
      
              init(viewModel: MyViewModel, asserts: [AssertFunc]) {
                  self.viewModel = viewModel
                  self.asserts = ArraySlice(asserts)
                  self.next = asserts.first
                  viewModel.$viewState
                      .sink { newViewState in
                          self.next?(newViewState)
                          self.next = self.asserts.dropFirst().first
                      }
              }
          }
      
      

      您可以像这样设置模拟:

      let mockView = MockView(
          viewModel: viewModel, 
          asserts: [
              { state in 
                  XCTAssertEqual(state.isLoading, false)
                  XCTAssertEqual(state.didError, false)
              },
              { state in 
                  XCTAssertEqual(state.isLoading, true)
                  ...
              },
              ...
          ])
      
      

      您还可以在断言函数中使用 XCT 期望。

      然后,在您的测试中,您创建视图模型、模拟数据服务和配置的模拟视图。

        let mockDataService: DataServiceProtocol = MockDataService
        let viewModel = ViewModel(dataService: mockDataService)
        let mockView = MockView(
          viewModel: viewModel, 
          asserts: [
              { state in 
                  XCTAssertEqual(state.isLoading, false)
                  XCTAssertEqual(state.didError, false)
              },
              ...
              { state in 
                  XCTAssertEqual(state.isLoading, false)
                  XCTAssertEqual(state.didError, false)
                  expectFinished.fulfill()
              },
              ...
          ])
      
          viewModel.getSomeData()
          
          // wait for the expectations
      

      注意:我没有编译或运行代码。

      你也可以看看Entwine

      【讨论】:

        猜你喜欢
        • 2014-09-02
        • 2014-08-30
        • 1970-01-01
        • 2016-02-19
        • 1970-01-01
        • 1970-01-01
        • 2020-08-27
        • 2020-11-24
        • 1970-01-01
        相关资源
        最近更新 更多