CombineSchedulers Documentation

Structure Failing​Scheduler

public struct FailingScheduler<SchedulerTimeType, SchedulerOptions>: Scheduler
where
  SchedulerTimeType: Strideable,
  SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible

A scheduler that causes the current XCTest test case to fail if it is used.

This scheduler can provide an additional layer of certainty that a tested code path does not require the use of a scheduler.

As a view model becomes more complex, only some of its logic may require a scheduler. When writing unit tests for any logic that does not require a scheduler, one should provide a failing scheduler, instead. This documents, directly in the test, that the feature does not use a scheduler. If it did, or ever does in the future, the test will fail.

For example, the following view model has a couple responsibilities:

class EpisodeViewModel: ObservableObject {
  @Published var episode: Episode?

  let apiClient: ApiClient
  let mainQueue: AnySchedulerOf<DispatchQueue>

  init(apiClient: ApiClient, mainQueue: AnySchedulerOf<DispatchQueue>) {
    self.apiClient = apiClient
    self.mainQueue = mainQueue
  }

  func reloadButtonTapped() {
    self.apiClient.fetchEpisode()
      .receive(on: self.mainQueue)
      .assign(to: &self.$episode)
  }

  func favoriteButtonTapped() {
    self.episode?.isFavorite.toggle()
  }
}
  • It lets the user tap a button to refresh some episode data

  • It lets the user toggle if the episode is one of their favorites

The API client delivers the episode on a background queue, so the view model must receive it on its main queue before mutating its state.

Tapping the favorite button, however, involves no scheduling. This means that a test can be written with a failing scheduler:

func testFavoriteButton() {
  let viewModel = EpisodeViewModel(
    apiClient: .mock,
    mainQueue: .failing
  )
  viewModel.episode = .mock

  viewModel.favoriteButtonTapped()
  XCTAssert(viewModel.episode?.isFavorite == true)

  viewModel.favoriteButtonTapped()
  XCTAssert(viewModel.episode?.isFavorite == false)
}

With .failing, this test pretty strongly declares that favoriting an episode does not need a scheduler to do the job, which means it is reasonable to assume that the feature is simple and does not involve any asynchrony.

In the future, should favoriting an episode fire off an API request that involves a scheduler, this test will begin to fail, which is a good thing! This will force us to address the complexity that was introduced. Had we used any other scheduler, it would quietly receive this additional work and the test would continue to pass.

%11 FailingScheduler FailingScheduler Scheduler Scheduler FailingScheduler->Scheduler

Conforms To

Scheduler

Initializers

init(_:​now:​)

public init(_ prefix: String = "", now: SchedulerTimeType)  

Creates a failing test scheduler with the given date.

Parameters

prefix String

A string that identifies this scheduler and will prefix all failure messages.

now Scheduler​Time​Type

now: The current date of the failing scheduler.

Properties

minimum​Tolerance

public var minimumTolerance: SchedulerTimeType.Stride  

now

public var now: SchedulerTimeType  

prefix

public let prefix: String

Methods

schedule(options:​_:​)

public func schedule(options _: SchedulerOptions?, _ action: () -> Void)  

schedule(after:​tolerance:​options:​_:​)

public func schedule(
    after _: SchedulerTimeType,
    tolerance _: SchedulerTimeType.Stride,
    options _: SchedulerOptions?,
    _ action: () -> Void
  )  

schedule(after:​interval:​tolerance:​options:​_:​)

public func schedule(
    after _: SchedulerTimeType,
    interval _: SchedulerTimeType.Stride,
    tolerance _: SchedulerTimeType.Stride,
    options _: SchedulerOptions?,
    _ action: () -> Void
  ) -> Cancellable