Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

16. Testing with RxTest
Written by Scott Gardner

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

👏🏼 💯🚀

☝🏼 That’s for you, for not skipping this chapter. Studies show that there are two reasons why developers skip writing tests:

  1. They write bug-free code.
  2. Writing tests isn’t fun.

If the first reason is all you, you’re hired! And if you agree with the second reason, well, let me introduce you to my little friend: RxTest.

For all the reasons why you started reading this book and are excited to begin using RxSwift in your app projects, RxTest and RxBlocking should get you excited to write tests against your RxSwift code, too. They provide an elegant API that makes writing tests easy and fun.

This chapter will introduce you to RxTest and RxBlocking. You’ll write tests against several RxSwift operators and production RxSwift code in an iOS app project.

Getting started

The starter project for this chapter is named Testing, and it contains a handy app to give you the red, green, and blue values and color name if available for the hex color code you enter. After running pod install, open up the project workspace and run it. You will see the app starts off with rayWenderlichGreen, but you can enter any hex color code and get the RGB and name values.

This app is organized using the MVVM design pattern, which you’ll learn about in Chapter 24, “MVVM with RxSwift.” The view model contains the following logic that the view controller will use to control the view, and you’ll write tests against this logic later in the chapter:

// Convert hex text to color
color = hexString
  .map { hex in
    guard hex.count == 7 else { return .clear }
    let color = UIColor(hex: hex)
    return color
  }
  .asDriver(onErrorJustReturn: .clear)

// Convert the color to an rgb tuple
rgb = color
  .map { color in
    var red: CGFloat = 0.0
    var green: CGFloat = 0.0
    var blue: CGFloat = 0.0

    color.getRed(&red, green: &green, blue: &blue, alpha: nil)
    let rgb = (Int(red * 255.0), Int(green * 255.0), Int(blue * 255.0))
    return rgb
  }
  .asDriver(onErrorJustReturn: (0, 0, 0))

// Convert the hex text to a matching name
colorName = hexString
  .map { hexString in
    let hex = String(hexString.dropFirst())

    if let color = ColorName(rawValue: hex) {
      return "\(color)"
    } else {
      return "--"
    }
  }
  .asDriver(onErrorJustReturn: "")

Before diving into testing this code, you’ll learn about RxTest by writing a few tests against RxSwift operators.

Note: This chapter presumes you are familiar with writing unit tests in iOS using XCTest. If you’re new to unit testing in iOS, check out our video course, Beginning iOS Unit and UI Testing at https://videos.raywenderlich.com/courses/57-beginning-ios-unit-and-ui-testing/lessons/1.

Testing operators with RxTest

RxTest is a separate library from RxSwift. It’s hosted within the RxSwift repo but requires a separate pod install and import. RxTest provides many useful additions for testing RxSwift code, including:

What are hot and cold observables?

RxSwift goes to great lengths to streamline and simplify your Rx code. There are circles of thought in the RxSwift community that feel hot and cold should be thought of as traits of observables instead of concrete types.

var scheduler: TestScheduler!
var subscription: Disposable!
override func setUp() {
  super.setUp()

  scheduler = TestScheduler(initialClock: 0)
}
override func tearDown() {
  scheduler.scheduleAt(1000) {
    self.subscription.dispose()
  }

  scheduler = nil
  super.tearDown()
}
// 1
func testAmb() {
  // 2
  let observer = scheduler.createObserver(String.self)
}
// 1
let observableA = scheduler.createHotObservable([
  // 2
  .next(100, "a"),
  .next(200, "b"),
  .next(300, "c")
])

// 3
let observableB = scheduler.createHotObservable([
  // 4
  .next(90, "1"),
  .next(200, "2"),
  .next(300, "3")
])
let ambObservable = observableA.amb(observableB)

self.subscription = ambObservable.subscribe(observer)
scheduler.start()
let results = observer.events.compactMap {
  $0.value.element
}
XCTAssertEqual(results, ["1", "2", "3"])

XCTAssertEqual(results, ["1", "2", "No you didn't!"])
XCTAssertEqual failed: ("["1", "2", "3"]") is not equal to ("["1", "2", "No you didn't!"]")
func testFilter() {
  // 1
  let observer = scheduler.createObserver(Int.self)

  // 2
  let observable = scheduler.createHotObservable([
    .next(100, 1),
    .next(200, 2),
    .next(300, 3),
    .next(400, 2),
    .next(500, 1)
  ])

  // 3
  let filterObservable = observable.filter {
    $0 < 3
  }

  // 4
  scheduler.scheduleAt(0) {
    self.subscription = filterObservable.subscribe(observer)
  }

  // 5
  scheduler.start()

  // 6
  let results = observer.events.compactMap {
    $0.value.element
  }

  // 7
  XCTAssertEqual(results, [1, 2, 2, 1])
}

Using RxBlocking

RxBlocking is another library housed within the RxSwift repo that has its own pod and must be separately imported. Its primary purpose is to convert an observable to a BlockingObservable via its toBlocking(timeout:) method. What this does is block the current thread until the observable terminates, either normally or by reaching the timeout. The timeout argument is an optional TimeInterval which is nil by default. If you set a value for timeout and that time interval elapses before the observable terminates normally, toBlocking will throw an RxError.timeout error. This essentially turns an asynchronous operation into a synchronous one, which makes testing much easier.

func testToArray() throws {
  // 1
  let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)

  // 2
  let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)

  // 3
  XCTAssertEqual(try toArrayObservable.toBlocking().toArray(), [1, 2])
}
public enum MaterializedSequenceResult<T> {
  case completed(elements: [T])
  case failed(elements: [T], error: Error)
}
func testToArrayMaterialized() {
  // 1
  let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)

  let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)

  // 2
  let result = toArrayObservable
    .toBlocking()
    .materialize()

  // 3
  switch result {
  case .completed(let elements):
    XCTAssertEqual(elements,  [1, 2])
  case .failed(_, let error):
    XCTFail(error.localizedDescription)
  }
}

Testing RxSwift production code

Start by opening ViewModel.swift in the Testing group. At the top, you’ll see these property definitions:

let hexString = BehaviorRelay(value: "")
let color: Driver<UIColor>
let rgb: Driver<(Int, Int, Int)>
let colorName: Driver<String>
enum ColorName: String {
  case aliceBlue = "F0F8FF"
  case antiqueWhite = "FAEBD7"
  case aqua = "0080FF"
  // And many more...
override func viewDidLoad() {
  super.viewDidLoad()

  configureUI()

  guard let textField = self.hexTextField else { return }

  textField.rx.text.orEmpty
    .bind(to: viewModel.hexString)
    .disposed(by: disposeBag)

  for button in buttons {
    button.rx.tap
      .bind {
        var shouldUpdate = false

        switch button.titleLabel!.text! {
        case "⊗":
          textField.text = "#"
          shouldUpdate = true
        case "←" where textField.text!.count > 1:
          textField.text = String(textField.text!.dropLast())
          shouldUpdate = true
        case "←":
          break
        case _ where textField.text!.count < 7:
          textField.text!.append(button.titleLabel!.text!)
          shouldUpdate = true
        default:
          break
        }

        if shouldUpdate {
          textField.sendActions(for: .valueChanged)
        }
      }
      .disposed(by: disposeBag)
  }

  viewModel.color
    .drive(onNext: { [unowned self] color in
      UIView.animate(withDuration: 0.2) {
        self.view.backgroundColor = color
      }
    })
    .disposed(by: disposeBag)

  viewModel.rgb
    .map { "\($0.0), \($0.1), \($0.2)" }
    .drive(rgbTextField.rx.text)
    .disposed(by: disposeBag)

  viewModel.colorName
    .drive(colorNameTextField.rx.text)
    .disposed(by: disposeBag)
}
override func setUp() {
  super.setUp()

  viewModel = ViewModel()
  scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}
func testColorIsRedWhenHexStringIsFF0000_async() {
  let disposeBag = DisposeBag()

  // 1
  let expect = expectation(description: #function)

  // 2
  let expectedColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)

  // 3
  var result: UIColor!
}
// 1
viewModel.color.asObservable()
  .skip(1)
  .subscribe(onNext: {
    // 2
    result = $0
    expect.fulfill()
  })
  .disposed(by: disposeBag)

// 3
viewModel.hexString.accept("#ff0000")

// 4
waitForExpectations(timeout: 1.0) { error in
  guard error == nil else {
    XCTFail(error!.localizedDescription)
    return
  }

  // 5
  XCTAssertEqual(expectedColor, result)
}
func testColorIsRedWhenHexStringIsFF0000() throws {
  // 1
  let colorObservable = viewModel.color.asObservable().subscribeOn(scheduler)

  // 2
  viewModel.hexString.accept("#ff0000")

  // 3
  XCTAssertEqual(try colorObservable.toBlocking(timeout: 1.0).first(),
                 .red)
}
func testRgbIs010WhenHexStringIs00FF00() throws {
  // 1
  let rgbObservable = viewModel.rgb.asObservable().subscribeOn(scheduler)

  // 2
  viewModel.hexString.accept("#00ff00")

  // 3
  let result = try rgbObservable.toBlocking().first()!

  XCTAssertEqual(0 * 255, result.0)
  XCTAssertEqual(1 * 255, result.1)
  XCTAssertEqual(0 * 255, result.2)
}
func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() throws {
  // 1
  let colorNameObservable = viewModel.colorName.asObservable().subscribeOn(scheduler)

  // 2
  viewModel.hexString.accept("#006636")

  // 3
  XCTAssertEqual("rayWenderlichGreen", try colorNameObservable.toBlocking().first()!)
}

Where to go from here?

Writing tests using RxText and RxBlocking is similar to writing data and UI binding code using RxSwift and RxCocoa. There are no challenges for this chapter, because you will be doing more view model testing in Chapter 24, “MVVM with RxSwift.” Happy testing!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now